/**
 * @license jCanvas v21.0.0
 * Copyright 2017 Caleb Evans
 * Released under the MIT license
 */
(function (jQuery, global, factory) {
  'use strict'

  if (typeof module === 'object' && typeof module.exports === 'object') {
    module.exports = function (jQuery, w) {
      return factory(jQuery, w)
    }
  } else {
    factory(jQuery, global)
  }

// Pass this if window is not defined yet
}(typeof window !== 'undefined' ? window.jQuery : {}, typeof window !== 'undefined' ? window : this, function ($, window) {
  'use strict'

  var document = window.document
  var Image = window.Image
  var Array = window.Array
  var getComputedStyle = window.getComputedStyle
  var Math = window.Math
  var Number = window.Number
  var parseFloat = window.parseFloat

  // Define local aliases to frequently used properties
  var defaults
  // Aliases to jQuery methods
  var extendObject = $.extend
  var inArray = $.inArray
  var typeOf = function (operand) {
    return Object.prototype.toString.call(operand)
      .slice(8, -1).toLowerCase()
  }
  var isPlainObject = $.isPlainObject
  // Math constants and functions
  var PI = Math.PI
  var round = Math.round
  var abs = Math.abs
  var sin = Math.sin
  var cos = Math.cos
  var atan2 = Math.atan2
  // The Array slice() method
  var arraySlice = Array.prototype.slice
  // jQuery's internal event normalization function
  var jQueryEventFix = $.event.fix
  // Object for storing a number of internal property maps
  var maps = {}
  // jQuery internal caches
  var caches = {
    dataCache: {},
    propCache: {},
    imageCache: {}
  }
  // Base transformations
  var baseTransforms = {
    rotate: 0,
    scaleX: 1,
    scaleY: 1,
    translateX: 0,
    translateY: 0,
    // Store all previous masks
    masks: []
  }
  // Object for storing CSS-related properties
  var css = {}
  var tangibleEvents = [
    'mousedown',
    'mousemove',
    'mouseup',
    'mouseover',
    'mouseout',
    'touchstart',
    'touchmove',
    'touchend'
  ]

  // Constructor for creating objects that inherit from jCanvas preferences and defaults
  function jCanvasObject (args) {
    var params = this
    var propName
    // Copy the given parameters into new object
    for (propName in args) {
      // Do not merge defaults into parameters
      if (Object.prototype.hasOwnProperty.call(args, propName)) {
        params[propName] = args[propName]
      }
    }
    return params
  }

  // jCanvas object in which global settings are other data are stored
  var jCanvas = {
    // Events object for storing jCanvas event initiation functions
    events: {},
    // Object containing all jCanvas event hooks
    eventHooks: {},
    // Settings for enabling future jCanvas features
    future: {}
  }

  // jCanvas default property values
  function jCanvasDefaults () {
    extendObject(this, jCanvasDefaults.baseDefaults)
  }
  jCanvasDefaults.baseDefaults = {
    align: 'center',
    arrowAngle: 90,
    arrowRadius: 0,
    autosave: true,
    baseline: 'middle',
    bringToFront: false,
    ccw: false,
    closed: false,
    compositing: 'source-over',
    concavity: 0,
    cornerRadius: 0,
    count: 1,
    cropFromCenter: true,
    crossOrigin: null,
    cursors: null,
    disableEvents: false,
    draggable: false,
    dragGroups: null,
    groups: null,
    data: null,
    dx: null,
    dy: null,
    end: 360,
    eventX: null,
    eventY: null,
    fillStyle: 'transparent',
    fontStyle: 'normal',
    fontSize: '12pt',
    fontFamily: 'sans-serif',
    fromCenter: true,
    height: null,
    imageSmoothing: true,
    inDegrees: true,
    intangible: false,
    index: null,
    letterSpacing: null,
    lineHeight: 1,
    layer: false,
    mask: false,
    maxWidth: null,
    miterLimit: 10,
    name: null,
    opacity: 1,
    r1: null,
    r2: null,
    radius: 0,
    repeat: 'repeat',
    respectAlign: false,
    restrictDragToAxis: null,
    rotate: 0,
    rounded: false,
    scale: 1,
    scaleX: 1,
    scaleY: 1,
    shadowBlur: 0,
    shadowColor: 'transparent',
    shadowStroke: false,
    shadowX: 0,
    shadowY: 0,
    sHeight: null,
    sides: 0,
    source: '',
    spread: 0,
    start: 0,
    strokeCap: 'butt',
    strokeDash: null,
    strokeDashOffset: 0,
    strokeJoin: 'miter',
    strokeStyle: 'transparent',
    strokeWidth: 1,
    sWidth: null,
    sx: null,
    sy: null,
    text: '',
    translate: 0,
    translateX: 0,
    translateY: 0,
    type: null,
    visible: true,
    width: null,
    x: 0,
    y: 0
  }
  defaults = new jCanvasDefaults()
  jCanvasObject.prototype = defaults

  /* Internal helper methods */

  // Determines if the given operand is a string
  function isString (operand) {
    return (typeOf(operand) === 'string')
  }

  // Determines if the given operand is a function
  function isFunction (operand) {
    return (typeOf(operand) === 'function')
  }

  // Determines if the given operand is numeric
  function isNumeric (operand) {
    return !isNaN(Number(operand)) && !isNaN(parseFloat(operand))
  }

  // Get 2D context for the given canvas
  function _getContext (canvas) {
    return (canvas && canvas.getContext ? canvas.getContext('2d') : null)
  }

  // Coerce designated number properties from strings to numbers
  function _coerceNumericProps (props) {
    var propName, propType, propValue
    // Loop through all properties in given property map
    for (propName in props) {
      if (Object.prototype.hasOwnProperty.call(props, propName)) {
        propValue = props[propName]
        propType = typeOf(propValue)
        // If property is non-empty string and value is numeric
        if (propType === 'string' && isNumeric(propValue) && propName !== 'text') {
          // Convert value to number
          props[propName] = parseFloat(propValue)
        }
      }
    }
    // Ensure value of text property is always a string
    if (props.text !== undefined) {
      props.text = String(props.text)
    }
  }

  // Clone the given transformations object
  function _cloneTransforms (transforms) {
    // Clone the object itself
    transforms = extendObject({}, transforms)
    // Clone the object's masks array
    transforms.masks = transforms.masks.slice(0)
    return transforms
  }

  // Save canvas context and update transformation stack
  function _saveCanvas (ctx, data) {
    var transforms
    ctx.save()
    transforms = _cloneTransforms(data.transforms)
    data.savedTransforms.push(transforms)
  }

  // Restore canvas context update transformation stack
  function _restoreCanvas (ctx, data) {
    if (data.savedTransforms.length === 0) {
      // Reset transformation state if it can't be restored any more
      data.transforms = _cloneTransforms(baseTransforms)
    } else {
      // Restore canvas context
      ctx.restore()
      // Restore current transform state to the last saved state
      data.transforms = data.savedTransforms.pop()
    }
  }

  // Set the style with the given name
  function _setStyle (canvas, ctx, params, styleName) {
    if (params[styleName]) {
      if (isFunction(params[styleName])) {
        // Handle functions
        ctx[styleName] = params[styleName].call(canvas, params)
      } else {
        // Handle string values
        ctx[styleName] = params[styleName]
      }
    }
  }

  // Set canvas context properties
  function _setGlobalProps (canvas, ctx, params) {
    _setStyle(canvas, ctx, params, 'fillStyle')
    _setStyle(canvas, ctx, params, 'strokeStyle')
    ctx.lineWidth = params.strokeWidth
    // Optionally round corners for paths
    if (params.rounded) {
      ctx.lineCap = ctx.lineJoin = 'round'
    } else {
      ctx.lineCap = params.strokeCap
      ctx.lineJoin = params.strokeJoin
      ctx.miterLimit = params.miterLimit
    }
    // Reset strokeDash if null
    if (!params.strokeDash) {
      params.strokeDash = []
    }
    // Dashed lines
    if (ctx.setLineDash) {
      ctx.setLineDash(params.strokeDash)
    }
    ctx.webkitLineDash = params.strokeDash
    ctx.lineDashOffset = ctx.webkitLineDashOffset = ctx.mozDashOffset = params.strokeDashOffset
    // Drop shadow
    ctx.shadowOffsetX = params.shadowX
    ctx.shadowOffsetY = params.shadowY
    ctx.shadowBlur = params.shadowBlur
    ctx.shadowColor = params.shadowColor
    // Opacity and composite operation
    ctx.globalAlpha = params.opacity
    ctx.globalCompositeOperation = params.compositing
    // Support cross-browser toggling of image smoothing
    if (params.imageSmoothing) {
      ctx.imageSmoothingEnabled = params.imageSmoothing
    }
  }

  // Optionally enable masking support for this path
  function _enableMasking (ctx, data, params) {
    if (params.mask) {
      // If jCanvas autosave is enabled
      if (params.autosave) {
        // Automatically save transformation state by default
        _saveCanvas(ctx, data)
      }
      // Clip the current path
      ctx.clip()
      // Keep track of current masks
      data.transforms.masks.push(params._args)
    }
  }

  // Restore individual shape transformation
  function _restoreTransform (ctx, params) {
    // If shape has been transformed by jCanvas
    if (params._transformed) {
      // Restore canvas context
      ctx.restore()
    }
  }

  // Close current canvas path
  function _closePath (canvas, ctx, params) {
    var data

    // Optionally close path
    if (params.closed) {
      ctx.closePath()
    }

    if (params.shadowStroke && params.strokeWidth !== 0) {
      // Extend the shadow to include the stroke of a drawing

      // Add a stroke shadow by stroking before filling
      ctx.stroke()
      ctx.fill()
      // Ensure the below stroking does not inherit a shadow
      ctx.shadowColor = 'transparent'
      ctx.shadowBlur = 0
      // Stroke over fill as usual
      ctx.stroke()
    } else {
      // If shadowStroke is not enabled, stroke & fill as usual

      ctx.fill()
      // Prevent extra shadow created by stroke (but only when fill is present)
      if (params.fillStyle !== 'transparent') {
        ctx.shadowColor = 'transparent'
      }
      if (params.strokeWidth !== 0) {
        // Only stroke if the stroke is not 0
        ctx.stroke()
      }
    }

    // Optionally close path
    if (!params.closed) {
      ctx.closePath()
    }

    // Restore individual shape transformation
    _restoreTransform(ctx, params)

    // Mask shape if chosen
    if (params.mask) {
      // Retrieve canvas data
      data = _getCanvasData(canvas)
      _enableMasking(ctx, data, params)
    }
  }

  // Transform (translate, scale, or rotate) shape
  function _transformShape (canvas, ctx, params, width, height) {
    // Get conversion factor for radians
    params._toRad = (params.inDegrees ? (PI / 180) : 1)

    params._transformed = true
    ctx.save()

    // Optionally measure (x, y) position from top-left corner
    if (!params.fromCenter && !params._centered && width !== undefined) {
      // Always draw from center unless otherwise specified
      if (height === undefined) {
        height = width
      }
      params.x += width / 2
      params.y += height / 2
      params._centered = true
    }
    // Optionally rotate shape
    if (params.rotate) {
      _rotateCanvas(ctx, params, null)
    }
    // Optionally scale shape
    if (params.scale !== 1 || params.scaleX !== 1 || params.scaleY !== 1) {
      _scaleCanvas(ctx, params, null)
    }
    // Optionally translate shape
    if (params.translate || params.translateX || params.translateY) {
      _translateCanvas(ctx, params, null)
    }
  }

  /* Plugin API */

  // Extend jCanvas with a user-defined method
  jCanvas.extend = function extend (plugin) {
    // Create plugin
    if (plugin.name) {
      // Merge properties with defaults
      if (plugin.props) {
        extendObject(defaults, plugin.props)
      }
      // Define plugin method
      $.fn[plugin.name] = function self (args) {
        var $canvases = this; var canvas; var e; var ctx
        var params

        for (e = 0; e < $canvases.length; e += 1) {
          canvas = $canvases[e]
          ctx = _getContext(canvas)
          if (ctx) {
            params = new jCanvasObject(args)
            _addLayer(canvas, params, args, self)

            _setGlobalProps(canvas, ctx, params)
            plugin.fn.call(canvas, ctx, params)
          }
        }
        return $canvases
      }
      // Add drawing type to drawing map
      if (plugin.type) {
        maps.drawings[plugin.type] = plugin.name
      }
    }
    return $.fn[plugin.name]
  }

  /* Layer API */

  // Retrieved the stored jCanvas data for a canvas element
  function _getCanvasData (canvas) {
    var dataCache = caches.dataCache; var data
    if (dataCache._canvas === canvas && dataCache._data) {
      // Retrieve canvas data from cache if possible
      data = dataCache._data
    } else {
      // Retrieve canvas data from jQuery's internal data storage
      data = $.data(canvas, 'jCanvas')
      if (!data) {
        // Create canvas data object if it does not already exist
        data = {
          // The associated canvas element
          canvas: canvas,
          // Layers array
          layers: [],
          // Layer maps
          layer: {
            names: {},
            groups: {}
          },
          eventHooks: {},
          // All layers that intersect with the event coordinates (regardless of visibility)
          intersecting: [],
          // The topmost layer whose area contains the event coordinates
          lastIntersected: null,
          cursor: $(canvas).css('cursor'),
          // Properties for the current drag event
          drag: {
            layer: null,
            dragging: false
          },
          // Data for the current event
          event: {
            type: null,
            x: null,
            y: null
          },
          // Events which already have been bound to the canvas
          events: {},
          // The canvas's current transformation state
          transforms: _cloneTransforms(baseTransforms),
          savedTransforms: [],
          // Whether a layer is being animated or not
          animating: false,
          // The layer currently being animated
          animated: null,
          // The device pixel ratio
          pixelRatio: 1,
          // Whether pixel ratio transformations have been applied
          scaled: false,
          // Whether the canvas should be redrawn when a layer mousemove
          // event triggers (either directly, or indirectly via dragging)
          redrawOnMousemove: false
        }
        // Use jQuery to store canvas data
        $.data(canvas, 'jCanvas', data)
      }
      // Cache canvas data for faster retrieval
      dataCache._canvas = canvas
      dataCache._data = data
    }
    return data
  }

  // Initialize all of a layer's associated jCanvas events
  function _addLayerEvents ($canvas, data, layer) {
    var eventName
    // Determine which jCanvas events need to be bound to this layer
    for (eventName in jCanvas.events) {
      if (Object.prototype.hasOwnProperty.call(jCanvas.events, eventName)) {
        // If layer has callback function to complement it
        if (layer[eventName] || (layer.cursors && layer.cursors[eventName])) {
          // Bind event to layer
          _addExplicitLayerEvent($canvas, data, layer, eventName)
        }
      }
    }
    if (!data.events.mouseout) {
      $canvas.bind('mouseout.jCanvas', function () {
        // Retrieve the layer whose drag event was canceled
        var layer = data.drag.layer; var l
        // If cursor mouses out of canvas while dragging
        if (layer) {
          // Cancel drag
          data.drag = {}
          _triggerLayerEvent($canvas, data, layer, 'dragcancel')
        }
        // Loop through all layers
        for (l = 0; l < data.layers.length; l += 1) {
          layer = data.layers[l]
          // If layer thinks it's still being moused over
          if (layer._hovered) {
            // Trigger mouseout on layer
            $canvas.triggerLayerEvent(data.layers[l], 'mouseout')
          }
        }
        // Redraw layers
        $canvas.drawLayers()
      })
      // Indicate that an event handler has been bound
      data.events.mouseout = true
    }
  }

  // Initialize the given event on the given layer
  function _addLayerEvent ($canvas, data, layer, eventName) {
    // Use touch events if appropriate
    // eventName = _getMouseEventName(eventName);
    // Bind event to layer
    jCanvas.events[eventName]($canvas, data)
    layer._event = true
  }

  // Add a layer event that was explicitly declared in the layer's parameter map,
  // excluding events added implicitly (e.g. mousemove event required by draggable
  // layers)
  function _addExplicitLayerEvent ($canvas, data, layer, eventName) {
    _addLayerEvent($canvas, data, layer, eventName)
    if (eventName === 'mouseover' || eventName === 'mouseout' || eventName === 'mousemove') {
      data.redrawOnMousemove = true
    }
  }

  // Enable drag support for this layer
  function _enableDrag ($canvas, data, layer) {
    var dragHelperEvents, eventName, i
    // Only make layer draggable if necessary
    if (layer.draggable || layer.cursors) {
      // Organize helper events which enable drag support
      dragHelperEvents = ['mousedown', 'mousemove', 'mouseup']

      // Bind each helper event to the canvas
      for (i = 0; i < dragHelperEvents.length; i += 1) {
        // Use touch events if appropriate
        eventName = dragHelperEvents[i]
        // Bind event
        _addLayerEvent($canvas, data, layer, eventName)
      }
      // Indicate that this layer has events bound to it
      layer._event = true
    }
  }

  // Update a layer property map if property is changed
  function _updateLayerName ($canvas, data, layer, props) {
    var nameMap = data.layer.names

    // If layer name is being added, not changed
    if (!props) {
      props = layer
    } else {
      // Remove old layer name entry because layer name has changed
      if (props.name !== undefined && isString(layer.name) && layer.name !== props.name) {
        delete nameMap[layer.name]
      }
    }

    // Add new entry to layer name map with new name
    if (isString(props.name)) {
      nameMap[props.name] = layer
    }
  }

  // Create or update the data map for the given layer and group type
  function _updateLayerGroups ($canvas, data, layer, props) {
    var groupMap = data.layer.groups
    var group; var groupName; var g
    var index; var l

    // If group name is not changing
    if (!props) {
      props = layer
    } else {
      // Remove layer from all of its associated groups
      if (props.groups !== undefined && layer.groups !== null) {
        for (g = 0; g < layer.groups.length; g += 1) {
          groupName = layer.groups[g]
          group = groupMap[groupName]
          if (group) {
            // Remove layer from its old layer group entry
            for (l = 0; l < group.length; l += 1) {
              if (group[l] === layer) {
                // Keep track of the layer's initial index
                index = l
                // Remove layer once found
                group.splice(l, 1)
                break
              }
            }
            // Remove layer group entry if group is empty
            if (group.length === 0) {
              delete groupMap[groupName]
            }
          }
        }
      }
    }

    // Add layer to new group if a new group name is given
    if (props.groups !== undefined && props.groups !== null) {
      for (g = 0; g < props.groups.length; g += 1) {
        groupName = props.groups[g]

        group = groupMap[groupName]
        if (!group) {
          // Create new group entry if it doesn't exist
          group = groupMap[groupName] = []
          group.name = groupName
        }
        if (index === undefined) {
          // Add layer to end of group unless otherwise stated
          index = group.length
        }
        // Add layer to its new layer group
        group.splice(index, 0, layer)
      }
    }
  }

  // Get event hooks object for the first selected canvas
  $.fn.getEventHooks = function getEventHooks () {
    var $canvases = this; var canvas; var data
    var eventHooks = {}

    if ($canvases.length !== 0) {
      canvas = $canvases[0]
      data = _getCanvasData(canvas)
      eventHooks = data.eventHooks
    }
    return eventHooks
  }

  // Set event hooks for the selected canvases
  $.fn.setEventHooks = function setEventHooks (eventHooks) {
    var $canvases = this; var e
    var data
    for (e = 0; e < $canvases.length; e += 1) {
      data = _getCanvasData($canvases[e])
      extendObject(data.eventHooks, eventHooks)
    }
    return $canvases
  }

  // Get jCanvas layers array
  $.fn.getLayers = function getLayers (callback) {
    var $canvases = this; var canvas; var data
    var layers; var layer; var l
    var matching = []

    if ($canvases.length !== 0) {
      canvas = $canvases[0]
      data = _getCanvasData(canvas)
      // Retrieve layers array for this canvas
      layers = data.layers

      // If a callback function is given
      if (isFunction(callback)) {
        // Filter the layers array using the callback
        for (l = 0; l < layers.length; l += 1) {
          layer = layers[l]
          if (callback.call(canvas, layer)) {
            // Add layer to array of matching layers if test passes
            matching.push(layer)
          }
        }
      } else {
        // Otherwise, get all layers

        matching = layers
      }
    }
    return matching
  }

  // Get a single jCanvas layer object
  $.fn.getLayer = function getLayer (layerId) {
    var $canvases = this; var canvas
    var data; var layers; var layer; var l
    var idType

    if ($canvases.length !== 0) {
      canvas = $canvases[0]
      data = _getCanvasData(canvas)
      layers = data.layers
      idType = typeOf(layerId)

      if (layerId && layerId.layer) {
        // Return the actual layer object if given
        layer = layerId
      } else if (idType === 'number') {
        // Retrieve the layer using the given index

        // Allow for negative indices
        if (layerId < 0) {
          layerId = layers.length + layerId
        }
        // Get layer with the given index
        layer = layers[layerId]
      } else if (idType === 'regexp') {
        // Get layer with the name that matches the given regex
        for (l = 0; l < layers.length; l += 1) {
          // Check if layer matches name
          if (isString(layers[l].name) && layers[l].name.match(layerId)) {
            layer = layers[l]
            break
          }
        }
      } else {
        // Get layer with the given name
        layer = data.layer.names[layerId]
      }
    }
    return layer
  }

  // Get all layers in the given group
  $.fn.getLayerGroup = function getLayerGroup (groupId) {
    var $canvases = this; var canvas; var data
    var groups; var groupName; var group
    var idType = typeOf(groupId)

    if ($canvases.length !== 0) {
      canvas = $canvases[0]

      if (idType === 'array') {
        // Return layer group if given
        group = groupId
      } else if (idType === 'regexp') {
        // Get canvas data
        data = _getCanvasData(canvas)
        groups = data.layer.groups
        // Loop through all layers groups for this canvas
        for (groupName in groups) {
          // Find a group whose name matches the given regex
          if (groupName.match(groupId)) {
            group = groups[groupName]
            // Stop after finding the first matching group
            break
          }
        }
      } else {
        // Find layer group with the given group name
        data = _getCanvasData(canvas)
        group = data.layer.groups[groupId]
      }
    }
    return group
  }

  // Get index of layer in layers array
  $.fn.getLayerIndex = function getLayerIndex (layerId) {
    var $canvases = this
    var layers = $canvases.getLayers()
    var layer = $canvases.getLayer(layerId)

    return inArray(layer, layers)
  }

  // Set properties of a layer
  $.fn.setLayer = function setLayer (layerId, props) {
    var $canvases = this; var $canvas; var e
    var data; var layer
    var propName; var propValue; var propType

    for (e = 0; e < $canvases.length; e += 1) {
      $canvas = $($canvases[e])
      data = _getCanvasData($canvases[e])

      layer = $($canvases[e]).getLayer(layerId)
      if (layer) {
        // Update layer property maps
        _updateLayerName($canvas, data, layer, props)
        _updateLayerGroups($canvas, data, layer, props)

        _coerceNumericProps(props)

        // Merge properties with layer
        for (propName in props) {
          if (Object.prototype.hasOwnProperty.call(props, propName)) {
            propValue = props[propName]
            propType = typeOf(propValue)
            if (propType === 'object' && isPlainObject(propValue)) {
              // Clone objects
              layer[propName] = extendObject({}, propValue)
              _coerceNumericProps(layer[propName])
            } else if (propType === 'array') {
              // Clone arrays
              layer[propName] = propValue.slice(0)
            } else if (propType === 'string') {
              if (propValue.indexOf('+=') === 0) {
                // Increment numbers prefixed with +=
                layer[propName] += parseFloat(propValue.substr(2))
              } else if (propValue.indexOf('-=') === 0) {
                // Decrement numbers prefixed with -=
                layer[propName] -= parseFloat(propValue.substr(2))
              } else if (!isNaN(propValue) && isNumeric(propValue) && propName !== 'text') {
                // Convert numeric values as strings to numbers
                layer[propName] = parseFloat(propValue)
              } else {
                // Otherwise, set given string value
                layer[propName] = propValue
              }
            } else {
              // Otherwise, set given value
              layer[propName] = propValue
            }
          }
        }

        // Update layer events
        _addLayerEvents($canvas, data, layer)
        _enableDrag($canvas, data, layer)

        // If layer's properties were changed
        if ($.isEmptyObject(props) === false) {
          _triggerLayerEvent($canvas, data, layer, 'change', props)
        }
      }
    }
    return $canvases
  }

  // Set properties of all layers (optionally filtered by a callback)
  $.fn.setLayers = function setLayers (props, callback) {
    var $canvases = this; var $canvas; var e
    var layers; var l
    for (e = 0; e < $canvases.length; e += 1) {
      $canvas = $($canvases[e])

      layers = $canvas.getLayers(callback)
      // Loop through all layers
      for (l = 0; l < layers.length; l += 1) {
        // Set properties of each layer
        $canvas.setLayer(layers[l], props)
      }
    }
    return $canvases
  }

  // Set properties of all layers in the given group
  $.fn.setLayerGroup = function setLayerGroup (groupId, props) {
    var $canvases = this; var $canvas; var e
    var group; var l

    for (e = 0; e < $canvases.length; e += 1) {
      // Get layer group
      $canvas = $($canvases[e])

      group = $canvas.getLayerGroup(groupId)
      // If group exists
      if (group) {
        // Loop through layers in group
        for (l = 0; l < group.length; l += 1) {
          // Merge given properties with layer
          $canvas.setLayer(group[l], props)
        }
      }
    }
    return $canvases
  }

  // Move a layer to the given index in the layers array
  $.fn.moveLayer = function moveLayer (layerId, index) {
    var $canvases = this; var $canvas; var e
    var data; var layers; var layer

    for (e = 0; e < $canvases.length; e += 1) {
      $canvas = $($canvases[e])
      data = _getCanvasData($canvases[e])

      // Retrieve layers array and desired layer
      layers = data.layers
      layer = $canvas.getLayer(layerId)
      if (layer) {
        // Ensure layer index is accurate
        layer.index = inArray(layer, layers)

        // Remove layer from its current placement
        layers.splice(layer.index, 1)
        // Add layer in its new placement
        layers.splice(index, 0, layer)

        // Handle negative indices
        if (index < 0) {
          index = layers.length + index
        }
        // Update layer's stored index
        layer.index = index

        _triggerLayerEvent($canvas, data, layer, 'move')
      }
    }
    return $canvases
  }

  // Remove a jCanvas layer
  $.fn.removeLayer = function removeLayer (layerId) {
    var $canvases = this; var $canvas; var e; var data
    var layers; var layer

    for (e = 0; e < $canvases.length; e += 1) {
      $canvas = $($canvases[e])
      data = _getCanvasData($canvases[e])

      // Retrieve layers array and desired layer
      layers = $canvas.getLayers()
      layer = $canvas.getLayer(layerId)
      // Remove layer if found
      if (layer) {
        // Ensure layer index is accurate
        layer.index = inArray(layer, layers)
        // Remove layer and allow it to be re-added later
        layers.splice(layer.index, 1)
        delete layer._layer

        // Update layer name map
        _updateLayerName($canvas, data, layer, {
          name: null
        })
        // Update layer group map
        _updateLayerGroups($canvas, data, layer, {
          groups: null
        })

        // Trigger 'remove' event
        _triggerLayerEvent($canvas, data, layer, 'remove')
      }
    }
    return $canvases
  }

  // Remove all layers
  $.fn.removeLayers = function removeLayers (callback) {
    var $canvases = this; var $canvas; var e
    var data; var layers; var layer; var l

    for (e = 0; e < $canvases.length; e += 1) {
      $canvas = $($canvases[e])
      data = _getCanvasData($canvases[e])
      layers = $canvas.getLayers(callback)
      // Remove all layers individually
      for (l = 0; l < layers.length; l += 1) {
        layer = layers[l]
        $canvas.removeLayer(layer)
        // Ensure no layer is skipped over
        l -= 1
      }
      // Update layer maps
      data.layer.names = {}
      data.layer.groups = {}
    }
    return $canvases
  }

  // Remove all layers in the group with the given ID
  $.fn.removeLayerGroup = function removeLayerGroup (groupId) {
    var $canvases = this; var $canvas; var e; var group; var l

    if (groupId !== undefined) {
      for (e = 0; e < $canvases.length; e += 1) {
        $canvas = $($canvases[e])

        group = $canvas.getLayerGroup(groupId)
        // Remove layer group using given group name
        if (group) {
          // Clone groups array
          group = group.slice(0)

          // Loop through layers in group
          for (l = 0; l < group.length; l += 1) {
            $canvas.removeLayer(group[l])
          }
        }
      }
    }
    return $canvases
  }

  // Add an existing layer to a layer group
  $.fn.addLayerToGroup = function addLayerToGroup (layerId, groupName) {
    var $canvases = this; var $canvas; var e
    var layer; var groups = [groupName]

    for (e = 0; e < $canvases.length; e += 1) {
      $canvas = $($canvases[e])
      layer = $canvas.getLayer(layerId)

      // If layer is not already in group
      if (layer.groups) {
        // Clone groups list
        groups = layer.groups.slice(0)
        // If layer is not already in group
        if (inArray(groupName, layer.groups) === -1) {
          // Add layer to group
          groups.push(groupName)
        }
      }
      // Update layer group maps
      $canvas.setLayer(layer, {
        groups: groups
      })
    }
    return $canvases
  }

  // Remove an existing layer from a layer group
  $.fn.removeLayerFromGroup = function removeLayerFromGroup (layerId, groupName) {
    var $canvases = this; var $canvas; var e
    var layer; var groups = []
    var index

    for (e = 0; e < $canvases.length; e += 1) {
      $canvas = $($canvases[e])
      layer = $canvas.getLayer(layerId)

      if (layer.groups) {
        // Find index of layer in group
        index = inArray(groupName, layer.groups)

        // If layer is in group
        if (index !== -1) {
          // Clone groups list
          groups = layer.groups.slice(0)

          // Remove layer from group
          groups.splice(index, 1)

          // Update layer group maps
          $canvas.setLayer(layer, {
            groups: groups
          })
        }
      }
    }
    return $canvases
  }

  // Get topmost layer that intersects with event coordinates
  function _getIntersectingLayer (data) {
    var layer, i,
      mask, m

    // Store the topmost layer
    layer = null

    // Get the topmost layer whose visible area intersects event coordinates
    for (i = data.intersecting.length - 1; i >= 0; i -= 1) {
      // Get current layer
      layer = data.intersecting[i]

      // If layer has previous masks
      if (layer._masks) {
        // Search previous masks to ensure
        // layer is visible at event coordinates
        for (m = layer._masks.length - 1; m >= 0; m -= 1) {
          mask = layer._masks[m]
          // If mask does not intersect event coordinates
          if (!mask.intersects) {
            // Indicate that the mask does not
            // intersect event coordinates
            layer.intersects = false
            // Stop searching previous masks
            break
          }
        }

        // If event coordinates intersect all previous masks
        // and layer is not intangible
        if (layer.intersects && !layer.intangible) {
          // Stop searching for topmost layer
          break
        }
      }
    }
    // If resulting layer is intangible
    if (layer && layer.intangible) {
      // Cursor does not intersect this layer
      layer = null
    }
    return layer
  }

  // Draw individual layer (internal)
  function _drawLayer ($canvas, ctx, layer, nextLayerIndex) {
    if (layer && layer.visible && layer._method) {
      if (nextLayerIndex) {
        layer._next = nextLayerIndex
      } else {
        layer._next = null
      }
      // If layer is an object, call its respective method
      if (layer._method) {
        layer._method.call($canvas, layer)
      }
    }
  }

  // Handle dragging of the currently-dragged layer
  function _handleLayerDrag ($canvas, data, eventType) {
    var layers, layer, l,
      drag, dragGroups,
      group, groupName, g,
      newX, newY

    drag = data.drag
    layer = drag.layer
    dragGroups = (layer && layer.dragGroups) || []
    layers = data.layers

    if (eventType === 'mousemove' || eventType === 'touchmove') {
      // Detect when user is currently dragging layer

      if (!drag.dragging) {
        // Detect when user starts dragging layer

        // Signify that a layer on the canvas is being dragged
        drag.dragging = true
        layer.dragging = true

        // Optionally bring layer to front when drag starts
        if (layer.bringToFront) {
          // Remove layer from its original position
          layers.splice(layer.index, 1)
          // Bring layer to front
          // push() returns the new array length
          layer.index = layers.push(layer)
        }

        // Set drag properties for this layer
        layer._startX = layer.x
        layer._startY = layer.y
        layer._endX = layer._eventX
        layer._endY = layer._eventY

        // Trigger dragstart event
        _triggerLayerEvent($canvas, data, layer, 'dragstart')
      }

      if (drag.dragging) {
        // Calculate position after drag
        newX = layer._eventX - (layer._endX - layer._startX)
        newY = layer._eventY - (layer._endY - layer._startY)
        if (layer.updateDragX) {
          newX = layer.updateDragX.call($canvas[0], layer, newX)
        }
        if (layer.updateDragY) {
          newY = layer.updateDragY.call($canvas[0], layer, newY)
        }
        layer.dx = newX - layer.x
        layer.dy = newY - layer.y
        if (layer.restrictDragToAxis !== 'y') {
          layer.x = newX
        }
        if (layer.restrictDragToAxis !== 'x') {
          layer.y = newY
        }

        // Trigger drag event
        _triggerLayerEvent($canvas, data, layer, 'drag')

        // Move groups with layer on drag
        for (g = 0; g < dragGroups.length; g += 1) {
          groupName = dragGroups[g]
          group = data.layer.groups[groupName]
          if (layer.groups && group) {
            for (l = 0; l < group.length; l += 1) {
              if (group[l] !== layer) {
                if (layer.restrictDragToAxis !== 'y' && group[l].restrictDragToAxis !== 'y') {
                  group[l].x += layer.dx
                }
                if (layer.restrictDragToAxis !== 'x' && group[l].restrictDragToAxis !== 'x') {
                  group[l].y += layer.dy
                }
              }
            }
          }
        }
      }
    } else if (eventType === 'mouseup' || eventType === 'touchend') {
      // Detect when user stops dragging layer

      if (drag.dragging) {
        layer.dragging = false
        drag.dragging = false
        data.redrawOnMousemove = data.originalRedrawOnMousemove
        // Trigger dragstop event
        _triggerLayerEvent($canvas, data, layer, 'dragstop')
      }

      // Cancel dragging
      data.drag = {}
    }
  }

  // List of CSS3 cursors that need to be prefixed
  css.cursors = ['grab', 'grabbing', 'zoom-in', 'zoom-out']

  // Function to detect vendor prefix
  // Modified version of David Walsh's implementation
  // https://davidwalsh.name/vendor-prefix
  css.prefix = (function () {
    var styles = getComputedStyle(document.documentElement, '')
    var pre = (arraySlice
      .call(styles)
      .join('')
      .match(/-(moz|webkit|ms)-/) || (styles.OLink === '' && ['', 'o'])
    )[1]
    return '-' + pre + '-'
  })()

  // Set cursor on canvas
  function _setCursor ($canvas, layer, eventType) {
    var cursor
    if (layer.cursors) {
      // Retrieve cursor from cursors object if it exists
      cursor = layer.cursors[eventType]
    }
    // Prefix any CSS3 cursor
    if ($.inArray(cursor, css.cursors) !== -1) {
      cursor = css.prefix + cursor
    }
    // If cursor is defined
    if (cursor) {
      // Set canvas cursor
      $canvas.css({
        cursor: cursor
      })
    }
  }

  // Reset cursor on canvas
  function _resetCursor ($canvas, data) {
    $canvas.css({
      cursor: data.cursor
    })
  }

  // Run the given event callback with the given arguments
  function _runEventCallback ($canvas, layer, eventType, callbacks, arg) {
    // Prevent callback from firing recursively
    if (callbacks[eventType] && layer._running && !layer._running[eventType]) {
      // Signify the start of callback execution for this event
      layer._running[eventType] = true
      // Run event callback with the given arguments
      callbacks[eventType].call($canvas[0], layer, arg)
      // Signify the end of callback execution for this event
      layer._running[eventType] = false
    }
  }

  // Determine if the given layer can "legally" fire the given event
  function _layerCanFireEvent (layer, eventType) {
    // If events are disable and if
    // layer is tangible or event is not tangible
    return (!layer.disableEvents &&
            (!layer.intangible || $.inArray(eventType, tangibleEvents) === -1))
  }

  // Trigger the given event on the given layer
  function _triggerLayerEvent ($canvas, data, layer, eventType, arg) {
    // If layer can legally fire this event type
    if (_layerCanFireEvent(layer, eventType)) {
      // Do not set a custom cursor on layer mouseout
      if (eventType !== 'mouseout') {
        // Update cursor if one is defined for this event
        _setCursor($canvas, layer, eventType)
      }

      // Trigger the user-defined event callback
      _runEventCallback($canvas, layer, eventType, layer, arg)
      // Trigger the canvas-bound event hook
      _runEventCallback($canvas, layer, eventType, data.eventHooks, arg)
      // Trigger the global event hook
      _runEventCallback($canvas, layer, eventType, jCanvas.eventHooks, arg)
    }
  }

  // Manually trigger a layer event
  $.fn.triggerLayerEvent = function (layer, eventType) {
    var $canvases = this; var $canvas; var e
    var data

    for (e = 0; e < $canvases.length; e += 1) {
      $canvas = $($canvases[e])
      data = _getCanvasData($canvases[e])
      layer = $canvas.getLayer(layer)
      if (layer) {
        _triggerLayerEvent($canvas, data, layer, eventType)
      }
    }
    return $canvases
  }

  // Draw layer with the given ID
  $.fn.drawLayer = function drawLayer (layerId) {
    var $canvases = this; var e; var ctx
    var $canvas; var layer

    for (e = 0; e < $canvases.length; e += 1) {
      $canvas = $($canvases[e])
      ctx = _getContext($canvases[e])
      if (ctx) {
        layer = $canvas.getLayer(layerId)
        _drawLayer($canvas, ctx, layer)
      }
    }
    return $canvases
  }

  // Draw all layers (or, if given, only layers starting at an index)
  $.fn.drawLayers = function drawLayers (args) {
    var $canvases = this; var $canvas; var e; var ctx
    // Internal parameters for redrawing the canvas
    var params = args || {}
    // Other variables
    var layers; var layer; var lastLayer; var l; var index; var lastIndex
    var data; var eventCache; var eventType; var isImageLayer

    // The layer index from which to start redrawing the canvas
    index = params.index
    if (!index) {
      index = 0
    }

    for (e = 0; e < $canvases.length; e += 1) {
      $canvas = $($canvases[e])
      ctx = _getContext($canvases[e])
      if (ctx) {
        data = _getCanvasData($canvases[e])

        // Clear canvas first unless otherwise directed
        if (params.clear !== false) {
          $canvas.clearCanvas()
        }

        // If a completion callback was provided, save it to the canvas data
        // store so that the function can be passed to drawLayers() again
        // after any image layers have loaded
        if (params.complete) {
          data.drawLayersComplete = params.complete
        }

        // Cache the layers array
        layers = data.layers

        // Draw layers from first to last (bottom to top)
        for (l = index; l < layers.length; l += 1) {
          layer = layers[l]

          // Ensure layer index is up-to-date
          layer.index = l

          // Prevent any one event from firing excessively
          if (params.resetFire) {
            layer._fired = false
          }
          // Draw layer
          _drawLayer($canvas, ctx, layer, l + 1)
          // Store list of previous masks for each layer
          layer._masks = data.transforms.masks.slice(0)

          // Allow image layers to load before drawing successive layers
          if (layer._method === $.fn.drawImage && layer.visible) {
            isImageLayer = true
            break
          }
        }

        // If layer is an image layer
        if (isImageLayer) {
          // Stop and wait for drawImage() to resume drawLayers()
          continue
        }

        // Store the latest
        lastIndex = l

        // Run completion callback (if provided) once all layers have drawn
        if (params.complete) {
          params.complete.call($canvases[e])
          delete data.drawLayersComplete
        }

        // Get first layer that intersects with event coordinates
        layer = _getIntersectingLayer(data)

        eventCache = data.event
        eventType = eventCache.type

        // If jCanvas has detected a dragstart
        if (data.drag.layer) {
          // Handle dragging of layer
          _handleLayerDrag($canvas, data, eventType)
        }

        // Manage mouseout event
        lastLayer = data.lastIntersected
        if (lastLayer !== null && layer !== lastLayer && lastLayer._hovered && !lastLayer._fired && !data.drag.dragging) {
          data.lastIntersected = null
          lastLayer._fired = true
          lastLayer._hovered = false
          _triggerLayerEvent($canvas, data, lastLayer, 'mouseout')
          _resetCursor($canvas, data)
        }

        if (layer) {
          // Use mouse event callbacks if no touch event callbacks are given
          if (!layer[eventType]) {
            eventType = _getMouseEventName(eventType)
          }

          // Check events for intersecting layer
          if (layer._event && layer.intersects) {
            data.lastIntersected = layer

            // Detect mouseover events
            if ((layer.mouseover || layer.mouseout || layer.cursors) && !data.drag.dragging) {
              if (!layer._hovered && !layer._fired) {
                // Prevent events from firing excessively
                layer._fired = true
                layer._hovered = true
                _triggerLayerEvent($canvas, data, layer, 'mouseover')
              }
            }

            // Detect any other mouse event
            if (!layer._fired) {
              // Prevent event from firing twice unintentionally
              layer._fired = true
              eventCache.type = null

              _triggerLayerEvent($canvas, data, layer, eventType)
            }

            // Use the mousedown event to start drag
            if (layer.draggable && !layer.disableEvents && (eventType === 'mousedown' || eventType === 'touchstart')) {
              // Keep track of drag state
              data.drag.layer = layer
              data.originalRedrawOnMousemove = data.redrawOnMousemove
              data.redrawOnMousemove = true
            }
          }
        }

        // If cursor is not intersecting with any layer
        if (layer === null && !data.drag.dragging) {
          // Reset cursor to previous state
          _resetCursor($canvas, data)
        }

        // If the last layer has been drawn
        if (lastIndex === layers.length) {
          // Reset list of intersecting layers
          data.intersecting.length = 0
          // Reset transformation stack
          data.transforms = _cloneTransforms(baseTransforms)
          data.savedTransforms.length = 0
        }
      }
    }
    return $canvases
  }

  // Add a jCanvas layer (internal)
  function _addLayer (canvas, params, args, method) {
    var $canvas; var data
    var layers; var layer = (params._layer ? args : params)

    // Store arguments object for later use
    params._args = args

    // Convert all draggable drawings into jCanvas layers
    if (params.draggable || params.dragGroups) {
      params.layer = true
      params.draggable = true
    }

    // Determine the layer's type using the available information
    if (!params._method) {
      if (method) {
        params._method = method
      } else if (params.method) {
        params._method = $.fn[params.method]
      } else if (params.type) {
        params._method = $.fn[maps.drawings[params.type]]
      }
    }

    // If layer hasn't been added yet
    if (params.layer && !params._layer) {
      // Add layer to canvas

      $canvas = $(canvas)

      data = _getCanvasData(canvas)
      layers = data.layers

      // Do not add duplicate layers of same name
      if (layer.name === null || (isString(layer.name) && data.layer.names[layer.name] === undefined)) {
        // Convert number properties to numbers
        _coerceNumericProps(params)

        // Ensure layers are unique across canvases by cloning them
        layer = new jCanvasObject(params)
        layer.canvas = canvas
        // Indicate that this is a layer for future checks
        layer.layer = true
        layer._layer = true
        layer._running = {}
        // If layer stores user-defined data
        if (layer.data !== null) {
          // Clone object
          layer.data = extendObject({}, layer.data)
        } else {
          // Otherwise, create data object
          layer.data = {}
        }
        // If layer stores a list of associated groups
        if (layer.groups !== null) {
          // Clone list
          layer.groups = layer.groups.slice(0)
        } else {
          // Otherwise, create empty list
          layer.groups = []
        }

        // Update layer group maps
        _updateLayerName($canvas, data, layer)
        _updateLayerGroups($canvas, data, layer)

        // Check for any associated jCanvas events and enable them
        _addLayerEvents($canvas, data, layer)

        // Optionally enable drag-and-drop support and cursor support
        _enableDrag($canvas, data, layer)

        // Copy _event property to parameters object
        params._event = layer._event

        // Calculate width/height for text layers
        if (layer._method === $.fn.drawText) {
          $canvas.measureText(layer)
        }

        // Add layer to end of array if no index is specified
        if (layer.index === null) {
          layer.index = layers.length
        }

        // Add layer to layers array at specified index
        layers.splice(layer.index, 0, layer)

        // Store layer on parameters object
        params._args = layer

        // Trigger an 'add' event
        _triggerLayerEvent($canvas, data, layer, 'add')
      }
    } else if (!params.layer) {
      _coerceNumericProps(params)
    }

    return layer
  }

  // Add a jCanvas layer
  $.fn.addLayer = function addLayer (args) {
    var $canvases = this; var e; var ctx
    var params

    for (e = 0; e < $canvases.length; e += 1) {
      ctx = _getContext($canvases[e])
      if (ctx) {
        params = new jCanvasObject(args)
        params.layer = true
        _addLayer($canvases[e], params, args)
      }
    }
    return $canvases
  }

  /* Animation API */

  // Define properties used in both CSS and jCanvas
  css.props = [
    'width',
    'height',
    'opacity',
    'lineHeight'
  ]
  css.propsObj = {}

  // Hide/show jCanvas/CSS properties so they can be animated using jQuery
  function _showProps (obj) {
    var cssProp, p
    for (p = 0; p < css.props.length; p += 1) {
      cssProp = css.props[p]
      obj[cssProp] = obj['_' + cssProp]
    }
  }
  function _hideProps (obj, reset) {
    var cssProp, p
    for (p = 0; p < css.props.length; p += 1) {
      cssProp = css.props[p]
      // Hide property using same name with leading underscore
      if (obj[cssProp] !== undefined) {
        obj['_' + cssProp] = obj[cssProp]
        css.propsObj[cssProp] = true
        if (reset) {
          delete obj[cssProp]
        }
      }
    }
  }

  // Evaluate property values that are functions
  function _parseEndValues (canvas, layer, endValues) {
    var propName, propValue,
      subPropName, subPropValue
    // Loop through all properties in map of end values
    for (propName in endValues) {
      if (Object.prototype.hasOwnProperty.call(endValues, propName)) {
        propValue = endValues[propName]
        // If end value is function
        if (isFunction(propValue)) {
          // Call function and use its value as the end value
          endValues[propName] = propValue.call(canvas, layer, propName)
        }
        // If end value is an object
        if (typeOf(propValue) === 'object' && isPlainObject(propValue)) {
          // Prepare to animate properties in object
          for (subPropName in propValue) {
            if (Object.prototype.hasOwnProperty.call(propValue, subPropName)) {
              subPropValue = propValue[subPropName]
              // Store property's start value at top-level of layer
              if (layer[propName] !== undefined) {
                layer[propName + '.' + subPropName] = layer[propName][subPropName]
                // Store property's end value at top-level of end values map
                endValues[propName + '.' + subPropName] = subPropValue
              }
            }
          }
          // Delete sub-property of object as it's no longer needed
          delete endValues[propName]
        }
      }
    }
    return endValues
  }

  // Remove sub-property aliases from layer object
  function _removeSubPropAliases (layer) {
    var propName
    for (propName in layer) {
      if (Object.prototype.hasOwnProperty.call(layer, propName)) {
        if (propName.indexOf('.') !== -1) {
          delete layer[propName]
        }
      }
    }
  }

  // Convert a color value to an array of RGB values
  function _colorToRgbArray (color) {
    var originalColor; var elem
    var rgb = []
    var multiple = 1

    // Deal with complete transparency
    if (color === 'transparent') {
      color = 'rgba(0, 0, 0, 0)'
    } else if (color.match(/^([a-z]+|#[0-9a-f]+)$/gi)) {
      // Deal with hexadecimal colors and color names
      elem = document.head
      originalColor = elem.style.color
      elem.style.color = color
      color = $.css(elem, 'color')
      elem.style.color = originalColor
    }
    // Parse RGB string
    if (color.match(/^rgb/gi)) {
      rgb = color.match(/(\d+(\.\d+)?)/gi)
      // Deal with RGB percentages
      if (color.match(/%/gi)) {
        multiple = 2.55
      }
      rgb[0] *= multiple
      rgb[1] *= multiple
      rgb[2] *= multiple
      // Ad alpha channel if given
      if (rgb[3] !== undefined) {
        rgb[3] = parseFloat(rgb[3])
      } else {
        rgb[3] = 1
      }
    }
    return rgb
  }

  // Animate a hex or RGB color
  function _animateColor (fx) {
    var n = 3
    var i
    // Only parse start and end colors once
    if (typeOf(fx.start) !== 'array') {
      fx.start = _colorToRgbArray(fx.start)
      fx.end = _colorToRgbArray(fx.end)
    }
    fx.now = []

    // If colors are RGBA, animate transparency
    if (fx.start[3] !== 1 || fx.end[3] !== 1) {
      n = 4
    }

    // Calculate current frame for red, green, blue, and alpha
    for (i = 0; i < n; i += 1) {
      fx.now[i] = fx.start[i] + ((fx.end[i] - fx.start[i]) * fx.pos)
      // Only the red, green, and blue values must be integers
      if (i < 3) {
        fx.now[i] = round(fx.now[i])
      }
    }
    if (fx.start[3] !== 1 || fx.end[3] !== 1) {
      // Only use RGBA if RGBA colors are given
      fx.now = 'rgba(' + fx.now.join(',') + ')'
    } else {
      // Otherwise, animate as solid colors
      fx.now.slice(0, 3)
      fx.now = 'rgb(' + fx.now.join(',') + ')'
    }
    // Animate colors for both canvas layers and DOM elements
    if (fx.elem.nodeName) {
      fx.elem.style[fx.prop] = fx.now
    } else {
      fx.elem[fx.prop] = fx.now
    }
  }

  // Animate jCanvas layer
  $.fn.animateLayer = function animateLayer () {
    var $canvases = this; var $canvas; var e; var ctx
    var args = arraySlice.call(arguments, 0)
    var data; var layer; var props

    // Deal with all cases of argument placement
    /*
		0. layer name/index
		1. properties
		2. duration/options
		3. easing
		4. complete function
		5. step function
	*/

    if (typeOf(args[2]) === 'object') {
      // Accept an options object for animation
      args.splice(2, 0, args[2].duration || null)
      args.splice(3, 0, args[3].easing || null)
      args.splice(4, 0, args[4].complete || null)
      args.splice(5, 0, args[5].step || null)
    } else {
      if (args[2] === undefined) {
        // If object is the last argument
        args.splice(2, 0, null)
        args.splice(3, 0, null)
        args.splice(4, 0, null)
      } else if (isFunction(args[2])) {
        // If callback comes after object
        args.splice(2, 0, null)
        args.splice(3, 0, null)
      }
      if (args[3] === undefined) {
        // If duration is the last argument
        args[3] = null
        args.splice(4, 0, null)
      } else if (isFunction(args[3])) {
        // If callback comes after duration
        args.splice(3, 0, null)
      }
    }

    // Run callback function when animation completes
    function complete ($canvas, data, layer) {
      return function () {
        _showProps(layer)
        _removeSubPropAliases(layer)

        // Prevent multiple redraw loops
        if (!data.animating || data.animated === layer) {
          // Redraw layers on last frame
          $canvas.drawLayers()
        }

        // Signify the end of an animation loop
        layer._animating = false
        data.animating = false
        data.animated = null

        // If callback is defined
        if (args[4]) {
          // Run callback at the end of the animation
          args[4].call($canvas[0], layer)
        }

        _triggerLayerEvent($canvas, data, layer, 'animateend')
      }
    }

    // Redraw layers on every frame of the animation
    function step ($canvas, data, layer) {
      return function (now, fx) {
        var parts; var propName; var subPropName
        var hidden = false

        // If animated property has been hidden
        if (fx.prop[0] === '_') {
          hidden = true
          // Unhide property temporarily
          fx.prop = fx.prop.replace('_', '')
          layer[fx.prop] = layer['_' + fx.prop]
        }

        // If animating property of sub-object
        if (fx.prop.indexOf('.') !== -1) {
          parts = fx.prop.split('.')
          propName = parts[0]
          subPropName = parts[1]
          if (layer[propName]) {
            layer[propName][subPropName] = fx.now
          }
        }

        // Throttle animation to improve efficiency
        if (layer._pos !== fx.pos) {
          layer._pos = fx.pos

          // Signify the start of an animation loop
          if (!layer._animating && !data.animating) {
            layer._animating = true
            data.animating = true
            data.animated = layer
          }

          // Prevent multiple redraw loops
          if (!data.animating || data.animated === layer) {
            // Redraw layers for every frame
            $canvas.drawLayers()
          }
        }

        // If callback is defined
        if (args[5]) {
          // Run callback for each step of animation
          args[5].call($canvas[0], now, fx, layer)
        }

        _triggerLayerEvent($canvas, data, layer, 'animate', fx)

        // If property should be hidden during animation
        if (hidden) {
          // Hide property again
          fx.prop = '_' + fx.prop
        }
      }
    }

    for (e = 0; e < $canvases.length; e += 1) {
      $canvas = $($canvases[e])
      ctx = _getContext($canvases[e])
      if (ctx) {
        data = _getCanvasData($canvases[e])

        // If a layer object was passed, use it the layer to be animated
        layer = $canvas.getLayer(args[0])

        // Ignore layers that are functions
        if (layer && layer._method !== $.fn.draw) {
          // Do not modify original object
          props = extendObject({}, args[1])

          props = _parseEndValues($canvases[e], layer, props)

          // Bypass jQuery CSS Hooks for CSS properties (width, opacity, etc.)
          _hideProps(props, true)
          _hideProps(layer)

          // Fix for jQuery's vendor prefixing support, which affects how width/height/opacity are animated
          layer.style = css.propsObj

          // Animate layer
          $(layer).animate(props, {
            duration: args[2],
            easing: ($.easing[args[3]] ? args[3] : null),
            // When animation completes
            complete: complete($canvas, data, layer),
            // Redraw canvas for every animation frame
            step: step($canvas, data, layer)
          })
          _triggerLayerEvent($canvas, data, layer, 'animatestart')
        }
      }
    }
    return $canvases
  }

  // Animate all layers in a layer group
  $.fn.animateLayerGroup = function animateLayerGroup (groupId) {
    var $canvases = this; var $canvas; var e
    var args = arraySlice.call(arguments, 0)
    var group; var l
    for (e = 0; e < $canvases.length; e += 1) {
      $canvas = $($canvases[e])
      group = $canvas.getLayerGroup(groupId)
      if (group) {
        // Animate all layers in the group
        for (l = 0; l < group.length; l += 1) {
          // Replace first argument with layer
          args[0] = group[l]
          $canvas.animateLayer.apply($canvas, args)
        }
      }
    }
    return $canvases
  }

  // Delay layer animation by a given number of milliseconds
  $.fn.delayLayer = function delayLayer (layerId, duration) {
    var $canvases = this; var $canvas; var e
    var data; var layer
    duration = duration || 0

    for (e = 0; e < $canvases.length; e += 1) {
      $canvas = $($canvases[e])
      data = _getCanvasData($canvases[e])
      layer = $canvas.getLayer(layerId)
      // If layer exists
      if (layer) {
        // Delay animation
        $(layer).delay(duration)
        _triggerLayerEvent($canvas, data, layer, 'delay')
      }
    }
    return $canvases
  }

  // Delay animation all layers in a layer group
  $.fn.delayLayerGroup = function delayLayerGroup (groupId, duration) {
    var $canvases = this; var $canvas; var e
    var group; var layer; var l
    duration = duration || 0

    for (e = 0; e < $canvases.length; e += 1) {
      $canvas = $($canvases[e])

      group = $canvas.getLayerGroup(groupId)
      // Delay all layers in the group
      if (group) {
        for (l = 0; l < group.length; l += 1) {
          // Delay each layer in the group
          layer = group[l]
          $canvas.delayLayer(layer, duration)
        }
      }
    }
    return $canvases
  }

  // Stop layer animation
  $.fn.stopLayer = function stopLayer (layerId, clearQueue) {
    var $canvases = this; var $canvas; var e
    var data; var layer

    for (e = 0; e < $canvases.length; e += 1) {
      $canvas = $($canvases[e])
      data = _getCanvasData($canvases[e])
      layer = $canvas.getLayer(layerId)
      // If layer exists
      if (layer) {
        // Stop animation
        $(layer).stop(clearQueue)
        _triggerLayerEvent($canvas, data, layer, 'stop')
      }
    }
    return $canvases
  }

  // Stop animation of all layers in a layer group
  $.fn.stopLayerGroup = function stopLayerGroup (groupId, clearQueue) {
    var $canvases = this; var $canvas; var e
    var group; var layer; var l

    for (e = 0; e < $canvases.length; e += 1) {
      $canvas = $($canvases[e])

      group = $canvas.getLayerGroup(groupId)
      // Stop all layers in the group
      if (group) {
        for (l = 0; l < group.length; l += 1) {
          // Stop each layer in the group
          layer = group[l]
          $canvas.stopLayer(layer, clearQueue)
        }
      }
    }
    return $canvases
  }

  // Enable animation for color properties
  function _supportColorProps (props) {
    var p
    for (p = 0; p < props.length; p += 1) {
      $.fx.step[props[p]] = _animateColor
    }
  }

  // Enable animation for color properties
  _supportColorProps([
    'color',
    'backgroundColor',
    'borderColor',
    'borderTopColor',
    'borderRightColor',
    'borderBottomColor',
    'borderLeftColor',
    'fillStyle',
    'outlineColor',
    'strokeStyle',
    'shadowColor'
  ])

  /* Event API */

  // Map standard mouse events to touch events
  maps.touchEvents = {
    mousedown: 'touchstart',
    mouseup: 'touchend',
    mousemove: 'touchmove'
  }
  // Map standard touch events to mouse events
  maps.mouseEvents = {
    touchstart: 'mousedown',
    touchend: 'mouseup',
    touchmove: 'mousemove'
  }

  // Convert mouse event name to a corresponding touch event name (if possible)
  function _getTouchEventName (eventName) {
    // Detect touch event support
    if (maps.touchEvents[eventName]) {
      eventName = maps.touchEvents[eventName]
    }
    return eventName
  }
  // Convert touch event name to a corresponding mouse event name
  function _getMouseEventName (eventName) {
    if (maps.mouseEvents[eventName]) {
      eventName = maps.mouseEvents[eventName]
    }
    return eventName
  }

  // Bind event to jCanvas layer using standard jQuery events
  function _createEvent (eventName) {
    jCanvas.events[eventName] = function ($canvas, data) {
      var helperEventName, touchEventName, eventCache

      // Retrieve canvas's event cache
      eventCache = data.event

      // Both mouseover/mouseout events will be managed by a single mousemove event
      helperEventName = (eventName === 'mouseover' || eventName === 'mouseout') ? 'mousemove' : eventName
      touchEventName = _getTouchEventName(helperEventName)

      function eventCallback (event) {
        // Cache current mouse position and redraw layers
        eventCache.x = event.offsetX
        eventCache.y = event.offsetY
        eventCache.type = helperEventName
        eventCache.event = event
        // Redraw layers on every trigger of the event; don't redraw if at
        // least one layer is draggable and there are no layers with
        // explicit mouseover/mouseout/mousemove events
        if (event.type !== 'mousemove' || data.redrawOnMousemove || data.drag.dragging) {
          $canvas.drawLayers({
            resetFire: true
          })
        }
        // Prevent default event behavior
        event.preventDefault()
      }

      // Ensure the event is not bound more than once
      if (!data.events[helperEventName]) {
        // Bind one canvas event which handles all layer events of that type
        if (touchEventName !== helperEventName) {
          $canvas.bind(helperEventName + '.jCanvas ' + touchEventName + '.jCanvas', eventCallback)
        } else {
          $canvas.bind(helperEventName + '.jCanvas', eventCallback)
        }
        // Prevent this event from being bound twice
        data.events[helperEventName] = true
      }
    }
  }
  function _createEvents (eventNames) {
    var n
    for (n = 0; n < eventNames.length; n += 1) {
      _createEvent(eventNames[n])
    }
  }
  // Populate jCanvas events object with some standard events
  _createEvents([
    'click',
    'dblclick',
    'mousedown',
    'mouseup',
    'mousemove',
    'mouseover',
    'mouseout',
    'touchstart',
    'touchmove',
    'touchend',
    'pointerdown',
    'pointermove',
    'pointerup',
    'contextmenu'
  ])

  // Check if event fires when a drawing is drawn
  function _detectEvents (canvas, ctx, params) {
    var layer, data, eventCache, intersects,
      transforms, x, y, angle

    // Use the layer object stored by the given parameters object
    layer = params._args
    // Canvas must have event bindings
    if (layer) {
      data = _getCanvasData(canvas)
      eventCache = data.event
      if (eventCache.x !== null && eventCache.y !== null) {
        // Respect user-defined pixel ratio
        x = eventCache.x * data.pixelRatio
        y = eventCache.y * data.pixelRatio
        // Determine if the given coordinates are in the current path
        intersects = ctx.isPointInPath(x, y) || (ctx.isPointInStroke && ctx.isPointInStroke(x, y))
      }
      transforms = data.transforms

      // Allow callback functions to retrieve the mouse coordinates
      layer.eventX = eventCache.x
      layer.eventY = eventCache.y
      layer.event = eventCache.event

      // Adjust coordinates to match current canvas transformation

      // Keep track of some transformation values
      angle = data.transforms.rotate
      x = layer.eventX
      y = layer.eventY

      if (angle !== 0) {
        // Rotate coordinates if coordinate space has been rotated
        layer._eventX = (x * cos(-angle)) - (y * sin(-angle))
        layer._eventY = (y * cos(-angle)) + (x * sin(-angle))
      } else {
        // Otherwise, no calculations need to be made
        layer._eventX = x
        layer._eventY = y
      }

      // Scale coordinates
      layer._eventX /= transforms.scaleX
      layer._eventY /= transforms.scaleY

      // If layer intersects with cursor
      if (intersects) {
        // Add it to a list of layers that intersect with cursor
        data.intersecting.push(layer)
      }
      layer.intersects = Boolean(intersects)
    }
  }

  // Normalize offsetX and offsetY for all browsers
  $.event.fix = function (event) {
    var offset, originalEvent, touches

    event = jQueryEventFix.call($.event, event)
    originalEvent = event.originalEvent

    // originalEvent does not exist for manually-triggered events
    if (originalEvent) {
      touches = originalEvent.changedTouches

      // If offsetX and offsetY are not supported, define them
      if (event.pageX !== undefined && event.offsetX === undefined) {
        try {
          offset = $(event.currentTarget).offset()
          if (offset) {
            event.offsetX = event.pageX - offset.left
            event.offsetY = event.pageY - offset.top
          }
        } catch (error) {
          // Fail silently
        }
      } else if (touches) {
        try {
          // Enable offsetX and offsetY for mobile devices
          offset = $(event.currentTarget).offset()
          if (offset) {
            event.offsetX = touches[0].pageX - offset.left
            event.offsetY = touches[0].pageY - offset.top
          }
        } catch (error) {
          // Fail silently
        }
      }
    }
    return event
  }

  /* Drawing API */

  // Map drawing names with their respective method names
  maps.drawings = {
    arc: 'drawArc',
    bezier: 'drawBezier',
    ellipse: 'drawEllipse',
    function: 'draw',
    image: 'drawImage',
    line: 'drawLine',
    path: 'drawPath',
    polygon: 'drawPolygon',
    slice: 'drawSlice',
    quadratic: 'drawQuadratic',
    rectangle: 'drawRect',
    text: 'drawText',
    vector: 'drawVector',
    save: 'saveCanvas',
    restore: 'restoreCanvas',
    rotate: 'rotateCanvas',
    scale: 'scaleCanvas',
    translate: 'translateCanvas'
  }

  // Draws on canvas using a function
  $.fn.draw = function draw (args) {
    var $canvases = this; var e; var ctx
    var params = new jCanvasObject(args)

    // Draw using any other method
    if (maps.drawings[params.type] && params.type !== 'function') {
      $canvases[maps.drawings[params.type]](args)
    } else {
      for (e = 0; e < $canvases.length; e += 1) {
        ctx = _getContext($canvases[e])
        if (ctx) {
          params = new jCanvasObject(args)
          _addLayer($canvases[e], params, args, draw)
          if (params.visible) {
            if (params.fn) {
              // Call the given user-defined function
              params.fn.call($canvases[e], ctx, params)
            }
          }
        }
      }
    }
    return $canvases
  }

  // Clears canvas
  $.fn.clearCanvas = function clearCanvas (args) {
    var $canvases = this; var e; var ctx
    var params = new jCanvasObject(args)

    for (e = 0; e < $canvases.length; e += 1) {
      ctx = _getContext($canvases[e])
      if (ctx) {
        if (params.width === null || params.height === null) {
          // Clear entire canvas if width/height is not given

          // Reset current transformation temporarily to ensure that the entire canvas is cleared
          ctx.save()
          ctx.setTransform(1, 0, 0, 1, 0, 0)
          ctx.clearRect(0, 0, $canvases[e].width, $canvases[e].height)
          ctx.restore()
        } else {
          // Otherwise, clear the defined section of the canvas

          // Transform clear rectangle
          _addLayer($canvases[e], params, args, clearCanvas)
          _transformShape($canvases[e], ctx, params, params.width, params.height)
          ctx.clearRect(params.x - (params.width / 2), params.y - (params.height / 2), params.width, params.height)
          // Restore previous transformation
          _restoreTransform(ctx, params)
        }
      }
    }
    return $canvases
  }

  /* Transformation API */

  // Restores canvas
  $.fn.saveCanvas = function saveCanvas (args) {
    var $canvases = this; var e; var ctx
    var params; var data; var i

    for (e = 0; e < $canvases.length; e += 1) {
      ctx = _getContext($canvases[e])
      if (ctx) {
        data = _getCanvasData($canvases[e])

        params = new jCanvasObject(args)
        _addLayer($canvases[e], params, args, saveCanvas)

        // Restore a number of times using the given count
        for (i = 0; i < params.count; i += 1) {
          _saveCanvas(ctx, data)
        }
      }
    }
    return $canvases
  }

  // Restores canvas
  $.fn.restoreCanvas = function restoreCanvas (args) {
    var $canvases = this; var e; var ctx
    var params; var data; var i

    for (e = 0; e < $canvases.length; e += 1) {
      ctx = _getContext($canvases[e])
      if (ctx) {
        data = _getCanvasData($canvases[e])

        params = new jCanvasObject(args)
        _addLayer($canvases[e], params, args, restoreCanvas)

        // Restore a number of times using the given count
        for (i = 0; i < params.count; i += 1) {
          _restoreCanvas(ctx, data)
        }
      }
    }
    return $canvases
  }

  // Rotates canvas (internal)
  function _rotateCanvas (ctx, params, transforms) {
    // Get conversion factor for radians
    params._toRad = (params.inDegrees ? (PI / 180) : 1)

    // Rotate canvas using shape as center of rotation
    ctx.translate(params.x, params.y)
    ctx.rotate(params.rotate * params._toRad)
    ctx.translate(-params.x, -params.y)

    // If transformation data was given
    if (transforms) {
      // Update transformation data
      transforms.rotate += (params.rotate * params._toRad)
    }
  }

  // Scales canvas (internal)
  function _scaleCanvas (ctx, params, transforms) {
    // Scale both the x- and y- axis using the 'scale' property
    if (params.scale !== 1) {
      params.scaleX = params.scaleY = params.scale
    }

    // Scale canvas using shape as center of rotation
    ctx.translate(params.x, params.y)
    ctx.scale(params.scaleX, params.scaleY)
    ctx.translate(-params.x, -params.y)

    // If transformation data was given
    if (transforms) {
      // Update transformation data
      transforms.scaleX *= params.scaleX
      transforms.scaleY *= params.scaleY
    }
  }

  // Translates canvas (internal)
  function _translateCanvas (ctx, params, transforms) {
    // Translate both the x- and y-axis using the 'translate' property
    if (params.translate) {
      params.translateX = params.translateY = params.translate
    }

    // Translate canvas
    ctx.translate(params.translateX, params.translateY)

    // If transformation data was given
    if (transforms) {
      // Update transformation data
      transforms.translateX += params.translateX
      transforms.translateY += params.translateY
    }
  }

  // Rotates canvas
  $.fn.rotateCanvas = function rotateCanvas (args) {
    var $canvases = this; var e; var ctx
    var params; var data

    for (e = 0; e < $canvases.length; e += 1) {
      ctx = _getContext($canvases[e])
      if (ctx) {
        data = _getCanvasData($canvases[e])

        params = new jCanvasObject(args)
        _addLayer($canvases[e], params, args, rotateCanvas)

        // Autosave transformation state by default
        if (params.autosave) {
          // Automatically save transformation state by default
          _saveCanvas(ctx, data)
        }
        _rotateCanvas(ctx, params, data.transforms)
      }
    }
    return $canvases
  }

  // Scales canvas
  $.fn.scaleCanvas = function scaleCanvas (args) {
    var $canvases = this; var e; var ctx
    var params; var data

    for (e = 0; e < $canvases.length; e += 1) {
      ctx = _getContext($canvases[e])
      if (ctx) {
        data = _getCanvasData($canvases[e])

        params = new jCanvasObject(args)
        _addLayer($canvases[e], params, args, scaleCanvas)

        // Autosave transformation state by default
        if (params.autosave) {
          // Automatically save transformation state by default
          _saveCanvas(ctx, data)
        }
        _scaleCanvas(ctx, params, data.transforms)
      }
    }
    return $canvases
  }

  // Translates canvas
  $.fn.translateCanvas = function translateCanvas (args) {
    var $canvases = this; var e; var ctx
    var params; var data

    for (e = 0; e < $canvases.length; e += 1) {
      ctx = _getContext($canvases[e])
      if (ctx) {
        data = _getCanvasData($canvases[e])

        params = new jCanvasObject(args)
        _addLayer($canvases[e], params, args, translateCanvas)

        // Autosave transformation state by default
        if (params.autosave) {
          // Automatically save transformation state by default
          _saveCanvas(ctx, data)
        }
        _translateCanvas(ctx, params, data.transforms)
      }
    }
    return $canvases
  }

  /* Shape API */

  // Draws rectangle
  $.fn.drawRect = function drawRect (args) {
    var $canvases = this; var e; var ctx
    var params
    var x1; var y1
    var x2; var y2
    var r; var temp

    for (e = 0; e < $canvases.length; e += 1) {
      ctx = _getContext($canvases[e])
      if (ctx) {
        params = new jCanvasObject(args)
        _addLayer($canvases[e], params, args, drawRect)
        if (params.visible) {
          _transformShape($canvases[e], ctx, params, params.width, params.height)
          _setGlobalProps($canvases[e], ctx, params)

          ctx.beginPath()
          if (params.width && params.height) {
            x1 = params.x - (params.width / 2)
            y1 = params.y - (params.height / 2)
            r = abs(params.cornerRadius)
            // If corner radius is defined and is not zero
            if (r) {
              // Draw rectangle with rounded corners if cornerRadius is defined

              x2 = params.x + (params.width / 2)
              y2 = params.y + (params.height / 2)

              // Handle negative width
              if (params.width < 0) {
                temp = x1
                x1 = x2
                x2 = temp
              }
              // Handle negative height
              if (params.height < 0) {
                temp = y1
                y1 = y2
                y2 = temp
              }

              // Prevent over-rounded corners
              if ((x2 - x1) - (2 * r) < 0) {
                r = (x2 - x1) / 2
              }
              if ((y2 - y1) - (2 * r) < 0) {
                r = (y2 - y1) / 2
              }

              // Draw rectangle
              ctx.moveTo(x1 + r, y1)
              ctx.lineTo(x2 - r, y1)
              ctx.arc(x2 - r, y1 + r, r, 3 * PI / 2, PI * 2, false)
              ctx.lineTo(x2, y2 - r)
              ctx.arc(x2 - r, y2 - r, r, 0, PI / 2, false)
              ctx.lineTo(x1 + r, y2)
              ctx.arc(x1 + r, y2 - r, r, PI / 2, PI, false)
              ctx.lineTo(x1, y1 + r)
              ctx.arc(x1 + r, y1 + r, r, PI, 3 * PI / 2, false)
              // Always close path
              params.closed = true
            } else {
              // Otherwise, draw rectangle with square corners
              ctx.rect(x1, y1, params.width, params.height)
            }
          }
          // Check for jCanvas events
          _detectEvents($canvases[e], ctx, params)
          // Close rectangle path
          _closePath($canvases[e], ctx, params)
        }
      }
    }
    return $canvases
  }

  // Retrieves a coterminal angle between 0 and 2pi for the given angle
  function _getCoterminal (angle) {
    while (angle < 0) {
      angle += (2 * PI)
    }
    return angle
  }

  // Retrieves the x-coordinate for the given angle in a circle
  function _getArcX (params, angle) {
    return params.x + (params.radius * cos(angle))
  }
  // Retrieves the y-coordinate for the given angle in a circle
  function _getArcY (params, angle) {
    return params.y + (params.radius * sin(angle))
  }

  // Draws arc (internal)
  function _drawArc (canvas, ctx, params, path) {
    var x1, y1, x2, y2,
      x3, y3, x4, y4,
      offsetX, offsetY,
      diff

    // Determine offset from dragging
    if (params === path) {
      offsetX = 0
      offsetY = 0
    } else {
      offsetX = params.x
      offsetY = params.y
    }

    // Convert default end angle to radians
    if (!path.inDegrees && path.end === 360) {
      path.end = PI * 2
    }

    // Convert angles to radians
    path.start *= params._toRad
    path.end *= params._toRad
    // Consider 0deg due north of arc
    path.start -= (PI / 2)
    path.end -= (PI / 2)

    // Ensure arrows are pointed correctly for CCW arcs
    diff = PI / 180
    if (path.ccw) {
      diff *= -1
    }

    // Calculate coordinates for start arrow
    x1 = _getArcX(path, path.start + diff)
    y1 = _getArcY(path, path.start + diff)
    x2 = _getArcX(path, path.start)
    y2 = _getArcY(path, path.start)

    _addStartArrow(
      canvas, ctx,
      params, path,
      x1, y1,
      x2, y2
    )

    // Draw arc
    ctx.arc(path.x + offsetX, path.y + offsetY, path.radius, path.start, path.end, path.ccw)

    // Calculate coordinates for end arrow
    x3 = _getArcX(path, path.end + diff)
    y3 = _getArcY(path, path.end + diff)
    x4 = _getArcX(path, path.end)
    y4 = _getArcY(path, path.end)

    _addEndArrow(
      canvas, ctx,
      params, path,
      x4, y4,
      x3, y3
    )
  }

  // Draws arc or circle
  $.fn.drawArc = function drawArc (args) {
    var $canvases = this; var e; var ctx
    var params

    for (e = 0; e < $canvases.length; e += 1) {
      ctx = _getContext($canvases[e])
      if (ctx) {
        params = new jCanvasObject(args)
        _addLayer($canvases[e], params, args, drawArc)
        if (params.visible) {
          _transformShape($canvases[e], ctx, params, params.radius * 2)
          _setGlobalProps($canvases[e], ctx, params)

          ctx.beginPath()
          _drawArc($canvases[e], ctx, params, params)
          // Check for jCanvas events
          _detectEvents($canvases[e], ctx, params)
          // Optionally close path
          _closePath($canvases[e], ctx, params)
        }
      }
    }
    return $canvases
  }

  // Draws ellipse
  $.fn.drawEllipse = function drawEllipse (args) {
    var $canvases = this; var e; var ctx
    var params
    var controlW
    var controlH

    for (e = 0; e < $canvases.length; e += 1) {
      ctx = _getContext($canvases[e])
      if (ctx) {
        params = new jCanvasObject(args)
        _addLayer($canvases[e], params, args, drawEllipse)
        if (params.visible) {
          _transformShape($canvases[e], ctx, params, params.width, params.height)
          _setGlobalProps($canvases[e], ctx, params)

          // Calculate control width and height
          controlW = params.width * (4 / 3)
          controlH = params.height

          // Create ellipse using curves
          ctx.beginPath()
          ctx.moveTo(params.x, params.y - (controlH / 2))
          // Left side
          ctx.bezierCurveTo(params.x - (controlW / 2), params.y - (controlH / 2), params.x - (controlW / 2), params.y + (controlH / 2), params.x, params.y + (controlH / 2))
          // Right side
          ctx.bezierCurveTo(params.x + (controlW / 2), params.y + (controlH / 2), params.x + (controlW / 2), params.y - (controlH / 2), params.x, params.y - (controlH / 2))
          // Check for jCanvas events
          _detectEvents($canvases[e], ctx, params)
          // Always close path
          params.closed = true
          _closePath($canvases[e], ctx, params)
        }
      }
    }
    return $canvases
  }

  // Draws a regular (equal-angled) polygon
  $.fn.drawPolygon = function drawPolygon (args) {
    var $canvases = this; var e; var ctx
    var params
    var theta; var dtheta; var hdtheta
    var apothem
    var x; var y; var i

    for (e = 0; e < $canvases.length; e += 1) {
      ctx = _getContext($canvases[e])
      if (ctx) {
        params = new jCanvasObject(args)
        _addLayer($canvases[e], params, args, drawPolygon)
        if (params.visible) {
          _transformShape($canvases[e], ctx, params, params.radius * 2)
          _setGlobalProps($canvases[e], ctx, params)

          // Polygon's central angle
          dtheta = (2 * PI) / params.sides
          // Half of dtheta
          hdtheta = dtheta / 2
          // Polygon's starting angle
          theta = hdtheta + (PI / 2)
          // Distance from polygon's center to the middle of its side
          apothem = params.radius * cos(hdtheta)

          // Calculate path and draw
          ctx.beginPath()
          for (i = 0; i < params.sides; i += 1) {
            // Draw side of polygon
            x = params.x + (params.radius * cos(theta))
            y = params.y + (params.radius * sin(theta))

            // Plot point on polygon
            ctx.lineTo(x, y)

            // Project side if chosen
            if (params.concavity) {
              // Sides are projected from the polygon's apothem
              x = params.x + ((apothem + (-apothem * params.concavity)) * cos(theta + hdtheta))
              y = params.y + ((apothem + (-apothem * params.concavity)) * sin(theta + hdtheta))
              ctx.lineTo(x, y)
            }

            // Increment theta by delta theta
            theta += dtheta
          }
          // Check for jCanvas events
          _detectEvents($canvases[e], ctx, params)
          // Always close path
          params.closed = true
          _closePath($canvases[e], ctx, params)
        }
      }
    }
    return $canvases
  }

  // Draws pie-shaped slice
  $.fn.drawSlice = function drawSlice (args) {
    var $canvases = this; var e; var ctx
    var params
    var angle; var dx; var dy

    for (e = 0; e < $canvases.length; e += 1) {
      ctx = _getContext($canvases[e])
      if (ctx) {
        params = new jCanvasObject(args)
        _addLayer($canvases[e], params, args, drawSlice)
        if (params.visible) {
          _transformShape($canvases[e], ctx, params, params.radius * 2)
          _setGlobalProps($canvases[e], ctx, params)

          // Perform extra calculations

          // Convert angles to radians
          params.start *= params._toRad
          params.end *= params._toRad
          // Consider 0deg at north of arc
          params.start -= (PI / 2)
          params.end -= (PI / 2)

          // Find positive equivalents of angles
          params.start = _getCoterminal(params.start)
          params.end = _getCoterminal(params.end)
          // Ensure start angle is less than end angle
          if (params.end < params.start) {
            params.end += (2 * PI)
          }

          // Calculate angular position of slice
          angle = ((params.start + params.end) / 2)

          // Calculate ratios for slice's angle
          dx = (params.radius * params.spread * cos(angle))
          dy = (params.radius * params.spread * sin(angle))

          // Adjust position of slice
          params.x += dx
          params.y += dy

          // Draw slice
          ctx.beginPath()
          ctx.arc(params.x, params.y, params.radius, params.start, params.end, params.ccw)
          ctx.lineTo(params.x, params.y)
          // Check for jCanvas events
          _detectEvents($canvases[e], ctx, params)
          // Always close path
          params.closed = true
          _closePath($canvases[e], ctx, params)
        }
      }
    }
    return $canvases
  }

  /* Path API */

  // Adds arrow to path using the given properties
  function _addArrow (canvas, ctx, params, path, x1, y1, x2, y2) {
    var leftX, leftY,
      rightX, rightY,
      offsetX, offsetY,
      angle

    // If arrow radius is given and path is not closed
    if (path.arrowRadius && !params.closed) {
      // Calculate angle
      angle = atan2((y2 - y1), (x2 - x1))
      // Adjust angle correctly
      angle -= PI
      // Calculate offset to place arrow at edge of path
      offsetX = (params.strokeWidth * cos(angle))
      offsetY = (params.strokeWidth * sin(angle))

      // Calculate coordinates for left half of arrow
      leftX = x2 + (path.arrowRadius * cos(angle + (path.arrowAngle / 2)))
      leftY = y2 + (path.arrowRadius * sin(angle + (path.arrowAngle / 2)))
      // Calculate coordinates for right half of arrow
      rightX = x2 + (path.arrowRadius * cos(angle - (path.arrowAngle / 2)))
      rightY = y2 + (path.arrowRadius * sin(angle - (path.arrowAngle / 2)))

      // Draw left half of arrow
      ctx.moveTo(leftX - offsetX, leftY - offsetY)
      ctx.lineTo(x2 - offsetX, y2 - offsetY)
      // Draw right half of arrow
      ctx.lineTo(rightX - offsetX, rightY - offsetY)

      // Visually connect arrow to path
      ctx.moveTo(x2 - offsetX, y2 - offsetY)
      ctx.lineTo(x2 + offsetX, y2 + offsetY)
      // Move back to end of path
      ctx.moveTo(x2, y2)
    }
  }

  // Optionally adds arrow to start of path
  function _addStartArrow (canvas, ctx, params, path, x1, y1, x2, y2) {
    if (!path._arrowAngleConverted) {
      path.arrowAngle *= params._toRad
      path._arrowAngleConverted = true
    }
    if (path.startArrow) {
      _addArrow(canvas, ctx, params, path, x1, y1, x2, y2)
    }
  }

  // Optionally adds arrow to end of path
  function _addEndArrow (canvas, ctx, params, path, x1, y1, x2, y2) {
    if (!path._arrowAngleConverted) {
      path.arrowAngle *= params._toRad
      path._arrowAngleConverted = true
    }
    if (path.endArrow) {
      _addArrow(canvas, ctx, params, path, x1, y1, x2, y2)
    }
  }

  // Draws line (internal)
  function _drawLine (canvas, ctx, params, path) {
    var l,
      lx, ly
    l = 2
    _addStartArrow(
      canvas, ctx,
      params, path,
      path.x2 + params.x,
      path.y2 + params.y,
      path.x1 + params.x,
      path.y1 + params.y
    )
    if (path.x1 !== undefined && path.y1 !== undefined) {
      ctx.moveTo(path.x1 + params.x, path.y1 + params.y)
    }
    while (true) {
      // Calculate next coordinates
      lx = path['x' + l]
      ly = path['y' + l]
      // If coordinates are given
      if (lx !== undefined && ly !== undefined) {
        // Draw next line
        ctx.lineTo(lx + params.x, ly + params.y)
        l += 1
      } else {
        // Otherwise, stop drawing
        break
      }
    }
    l -= 1
    // Optionally add arrows to path
    _addEndArrow(
      canvas, ctx,
      params,
      path,
      path['x' + (l - 1)] + params.x,
      path['y' + (l - 1)] + params.y,
      path['x' + l] + params.x,
      path['y' + l] + params.y
    )
  }

  // Draws line
  $.fn.drawLine = function drawLine (args) {
    var $canvases = this; var e; var ctx
    var params

    for (e = 0; e < $canvases.length; e += 1) {
      ctx = _getContext($canvases[e])
      if (ctx) {
        params = new jCanvasObject(args)
        _addLayer($canvases[e], params, args, drawLine)
        if (params.visible) {
          _transformShape($canvases[e], ctx, params)
          _setGlobalProps($canvases[e], ctx, params)

          // Draw each point
          ctx.beginPath()
          _drawLine($canvases[e], ctx, params, params)
          // Check for jCanvas events
          _detectEvents($canvases[e], ctx, params)
          // Optionally close path
          _closePath($canvases[e], ctx, params)
        }
      }
    }
    return $canvases
  }

  // Draws quadratic curve (internal)
  function _drawQuadratic (canvas, ctx, params, path) {
    var l,
      lx, ly,
      lcx, lcy

    l = 2

    _addStartArrow(
      canvas,
      ctx,
      params,
      path,
      path.cx1 + params.x,
      path.cy1 + params.y,
      path.x1 + params.x,
      path.y1 + params.y
    )

    if (path.x1 !== undefined && path.y1 !== undefined) {
      ctx.moveTo(path.x1 + params.x, path.y1 + params.y)
    }
    while (true) {
      // Calculate next coordinates
      lx = path['x' + l]
      ly = path['y' + l]
      lcx = path['cx' + (l - 1)]
      lcy = path['cy' + (l - 1)]
      // If coordinates are given
      if (lx !== undefined && ly !== undefined && lcx !== undefined && lcy !== undefined) {
        // Draw next curve
        ctx.quadraticCurveTo(lcx + params.x, lcy + params.y, lx + params.x, ly + params.y)
        l += 1
      } else {
        // Otherwise, stop drawing
        break
      }
    }
    l -= 1
    _addEndArrow(
      canvas,
      ctx,
      params,
      path,
      path['cx' + (l - 1)] + params.x,
      path['cy' + (l - 1)] + params.y,
      path['x' + l] + params.x,
      path['y' + l] + params.y
    )
  }

  // Draws quadratic curve
  $.fn.drawQuadratic = function drawQuadratic (args) {
    var $canvases = this; var e; var ctx
    var params

    for (e = 0; e < $canvases.length; e += 1) {
      ctx = _getContext($canvases[e])
      if (ctx) {
        params = new jCanvasObject(args)
        _addLayer($canvases[e], params, args, drawQuadratic)
        if (params.visible) {
          _transformShape($canvases[e], ctx, params)
          _setGlobalProps($canvases[e], ctx, params)

          // Draw each point
          ctx.beginPath()
          _drawQuadratic($canvases[e], ctx, params, params)
          // Check for jCanvas events
          _detectEvents($canvases[e], ctx, params)
          // Optionally close path
          _closePath($canvases[e], ctx, params)
        }
      }
    }
    return $canvases
  }

  // Draws Bezier curve (internal)
  function _drawBezier (canvas, ctx, params, path) {
    var l, lc,
      lx, ly,
      lcx1, lcy1,
      lcx2, lcy2

    l = 2
    lc = 1

    _addStartArrow(
      canvas,
      ctx,
      params,
      path,
      path.cx1 + params.x,
      path.cy1 + params.y,
      path.x1 + params.x,
      path.y1 + params.y
    )

    if (path.x1 !== undefined && path.y1 !== undefined) {
      ctx.moveTo(path.x1 + params.x, path.y1 + params.y)
    }
    while (true) {
      // Calculate next coordinates
      lx = path['x' + l]
      ly = path['y' + l]
      lcx1 = path['cx' + lc]
      lcy1 = path['cy' + lc]
      lcx2 = path['cx' + (lc + 1)]
      lcy2 = path['cy' + (lc + 1)]
      // If next coordinates are given
      if (lx !== undefined && ly !== undefined && lcx1 !== undefined && lcy1 !== undefined && lcx2 !== undefined && lcy2 !== undefined) {
        // Draw next curve
        ctx.bezierCurveTo(lcx1 + params.x, lcy1 + params.y, lcx2 + params.x, lcy2 + params.y, lx + params.x, ly + params.y)
        l += 1
        lc += 2
      } else {
        // Otherwise, stop drawing
        break
      }
    }
    l -= 1
    lc -= 2
    _addEndArrow(
      canvas,
      ctx,
      params,
      path,
      path['cx' + (lc + 1)] + params.x,
      path['cy' + (lc + 1)] + params.y,
      path['x' + l] + params.x,
      path['y' + l] + params.y
    )
  }

  // Draws Bezier curve
  $.fn.drawBezier = function drawBezier (args) {
    var $canvases = this; var e; var ctx
    var params

    for (e = 0; e < $canvases.length; e += 1) {
      ctx = _getContext($canvases[e])
      if (ctx) {
        params = new jCanvasObject(args)
        _addLayer($canvases[e], params, args, drawBezier)
        if (params.visible) {
          _transformShape($canvases[e], ctx, params)
          _setGlobalProps($canvases[e], ctx, params)

          // Draw each point
          ctx.beginPath()
          _drawBezier($canvases[e], ctx, params, params)
          // Check for jCanvas events
          _detectEvents($canvases[e], ctx, params)
          // Optionally close path
          _closePath($canvases[e], ctx, params)
        }
      }
    }
    return $canvases
  }

  // Retrieves the x-coordinate for the given vector angle and length
  function _getVectorX (params, angle, length) {
    angle *= params._toRad
    angle -= (PI / 2)
    return (length * cos(angle))
  }
  // Retrieves the y-coordinate for the given vector angle and length
  function _getVectorY (params, angle, length) {
    angle *= params._toRad
    angle -= (PI / 2)
    return (length * sin(angle))
  }

  // Draws vector (internal) #2
  function _drawVector (canvas, ctx, params, path) {
    var l, angle, length,
      offsetX, offsetY,
      x, y,
      x3, y3,
      x4, y4

    // Determine offset from dragging
    if (params === path) {
      offsetX = 0
      offsetY = 0
    } else {
      offsetX = params.x
      offsetY = params.y
    }

    l = 1
    x = x3 = x4 = path.x + offsetX
    y = y3 = y4 = path.y + offsetY

    _addStartArrow(
      canvas, ctx,
      params, path,
      x + _getVectorX(params, path.a1, path.l1),
      y + _getVectorY(params, path.a1, path.l1),
      x,
      y
    )

    // The vector starts at the given (x, y) coordinates
    if (path.x !== undefined && path.y !== undefined) {
      ctx.moveTo(x, y)
    }
    while (true) {
      angle = path['a' + l]
      length = path['l' + l]

      if (angle !== undefined && length !== undefined) {
        // Convert the angle to radians with 0 degrees starting at north
        // Keep track of last two coordinates
        x3 = x4
        y3 = y4
        // Compute (x, y) coordinates from angle and length
        x4 += _getVectorX(params, angle, length)
        y4 += _getVectorY(params, angle, length)
        ctx.lineTo(x4, y4)
        l += 1
      } else {
        // Otherwise, stop drawing
        break
      }
    }
    _addEndArrow(
      canvas, ctx,
      params, path,
      x3, y3,
      x4, y4
    )
  }

  // Draws vector
  $.fn.drawVector = function drawVector (args) {
    var $canvases = this; var e; var ctx
    var params

    for (e = 0; e < $canvases.length; e += 1) {
      ctx = _getContext($canvases[e])
      if (ctx) {
        params = new jCanvasObject(args)
        _addLayer($canvases[e], params, args, drawVector)
        if (params.visible) {
          _transformShape($canvases[e], ctx, params)
          _setGlobalProps($canvases[e], ctx, params)

          // Draw each point
          ctx.beginPath()
          _drawVector($canvases[e], ctx, params, params)
          // Check for jCanvas events
          _detectEvents($canvases[e], ctx, params)
          // Optionally close path
          _closePath($canvases[e], ctx, params)
        }
      }
    }
    return $canvases
  }

  // Draws a path consisting of one or more subpaths
  $.fn.drawPath = function drawPath (args) {
    var $canvases = this; var e; var ctx
    var params
    var l; var lp

    for (e = 0; e < $canvases.length; e += 1) {
      ctx = _getContext($canvases[e])
      if (ctx) {
        params = new jCanvasObject(args)
        _addLayer($canvases[e], params, args, drawPath)
        if (params.visible) {
          _transformShape($canvases[e], ctx, params)
          _setGlobalProps($canvases[e], ctx, params)

          ctx.beginPath()
          l = 1
          while (true) {
            lp = params['p' + l]
            if (lp !== undefined) {
              lp = new jCanvasObject(lp)
              if (lp.type === 'line') {
                _drawLine($canvases[e], ctx, params, lp)
              } else if (lp.type === 'quadratic') {
                _drawQuadratic($canvases[e], ctx, params, lp)
              } else if (lp.type === 'bezier') {
                _drawBezier($canvases[e], ctx, params, lp)
              } else if (lp.type === 'vector') {
                _drawVector($canvases[e], ctx, params, lp)
              } else if (lp.type === 'arc') {
                _drawArc($canvases[e], ctx, params, lp)
              }
              l += 1
            } else {
              break
            }
          }

          // Check for jCanvas events
          _detectEvents($canvases[e], ctx, params)
          // Optionally close path
          _closePath($canvases[e], ctx, params)
        }
      }
    }
    return $canvases
  }

  /* Text API */

  // Calculates font string and set it as the canvas font
  function _setCanvasFont (canvas, ctx, params) {
    // Otherwise, use the given font attributes
    if (!isNaN(Number(params.fontSize))) {
      // Give font size units if it doesn't have any
      params.fontSize += 'px'
    }
    // Set font using given font properties
    ctx.font = params.fontStyle + ' ' + params.fontSize + ' ' + params.fontFamily
  }

  // Measures canvas text
  function _measureText (canvas, ctx, params, lines) {
    var originalSize; var curWidth; var l
    var propCache = caches.propCache

    // Used cached width/height if possible
    if (propCache.text === params.text && propCache.fontStyle === params.fontStyle && propCache.fontSize === params.fontSize && propCache.fontFamily === params.fontFamily && propCache.maxWidth === params.maxWidth && propCache.lineHeight === params.lineHeight) {
      params.width = propCache.width
      params.height = propCache.height
    } else {
      // Calculate text dimensions only once

      // Calculate width of first line (for comparison)
      params.width = ctx.measureText(lines[0]).width

      // Get width of longest line
      for (l = 1; l < lines.length; l += 1) {
        curWidth = ctx.measureText(lines[l]).width
        // Ensure text's width is the width of its longest line
        if (curWidth > params.width) {
          params.width = curWidth
        }
      }

      // Save original font size
      originalSize = canvas.style.fontSize
      // Temporarily set canvas font size to retrieve size in pixels
      canvas.style.fontSize = params.fontSize
      // Save text width and height in parameters object
      params.height = parseFloat($.css(canvas, 'fontSize')) * lines.length * params.lineHeight
      // Reset font size to original size
      canvas.style.fontSize = originalSize
    }
  }

  // Wraps a string of text within a defined width
  function _wrapText (ctx, params) {
    var allText = String(params.text)
    // Maximum line width (optional)
    var maxWidth = params.maxWidth
    // Lines created by manual line breaks (\n)
    var manualLines = allText.split('\n')
    // All lines created manually and by wrapping
    var allLines = []
    // Other variables
    var lines; var line; var l
    var text; var words; var w

    // Loop through manually-broken lines
    for (l = 0; l < manualLines.length; l += 1) {
      text = manualLines[l]
      // Split line into list of words
      words = text.split(' ')
      lines = []
      line = ''

      // If text is short enough initially
      // Or, if the text consists of only one word
      if (words.length === 1 || ctx.measureText(text).width < maxWidth) {
        // No need to wrap text
        lines = [text]
      } else {
        // Wrap lines
        for (w = 0; w < words.length; w += 1) {
          // Once line gets too wide, push word to next line
          if (ctx.measureText(line + words[w]).width > maxWidth) {
            // This check prevents empty lines from being created
            if (line !== '') {
              lines.push(line)
            }
            // Start new line and repeat process
            line = ''
          }
          // Add words to line until the line is too wide
          line += words[w]
          // Do not add a space after the last word
          if (w !== (words.length - 1)) {
            line += ' '
          }
        }
        // The last word should always be pushed
        lines.push(line)
      }
      // Remove extra space at the end of each line
      allLines = allLines.concat(
        lines
          .join('\n')
          .replace(/((\n))|($)/gi, '$2')
          .split('\n')
      )
    }

    return allLines
  }

  // Draws text on canvas
  $.fn.drawText = function drawText (args) {
    var $canvases = this; var e; var ctx
    var params; var layer
    var lines; var line; var l
    var fontSize; var constantCloseness = 500
    var nchars; var chars; var ch; var c
    var x; var y

    for (e = 0; e < $canvases.length; e += 1) {
      ctx = _getContext($canvases[e])
      if (ctx) {
        params = new jCanvasObject(args)
        _addLayer($canvases[e], params, args, drawText)
        if (params.visible) {
          // Set text-specific properties
          ctx.textBaseline = params.baseline
          ctx.textAlign = params.align

          // Set canvas font using given properties
          _setCanvasFont($canvases[e], ctx, params)

          if (params.maxWidth !== null) {
            // Wrap text using an internal function
            lines = _wrapText(ctx, params)
          } else {
            // Convert string of text to list of lines
            lines = params.text
              .toString()
              .split('\n')
          }

          // Calculate text's width and height
          _measureText($canvases[e], ctx, params, lines)

          // If text is a layer
          if (layer) {
            // Copy calculated width/height to layer object
            layer.width = params.width
            layer.height = params.height
          }

          _transformShape($canvases[e], ctx, params, params.width, params.height)
          _setGlobalProps($canvases[e], ctx, params)

          // Adjust text position to accomodate different horizontal alignments
          x = params.x
          if (params.align === 'left') {
            if (params.respectAlign) {
              // Realign text to the left if chosen
              params.x += params.width / 2
            } else {
              // Center text block by default
              x -= params.width / 2
            }
          } else if (params.align === 'right') {
            if (params.respectAlign) {
              // Realign text to the right if chosen
              params.x -= params.width / 2
            } else {
              // Center text block by default
              x += params.width / 2
            }
          }

          if (params.radius) {
            fontSize = parseFloat(params.fontSize)

            // Greater values move clockwise
            if (params.letterSpacing === null) {
              params.letterSpacing = fontSize / constantCloseness
            }

            // Loop through each line of text
            for (l = 0; l < lines.length; l += 1) {
              ctx.save()
              ctx.translate(params.x, params.y)
              line = lines[l]
              if (params.flipArcText) {
                chars = line.split('')
                chars.reverse()
                line = chars.join('')
              }
              nchars = line.length
              ctx.rotate(-(PI * params.letterSpacing * (nchars - 1)) / 2)
              // Loop through characters on each line
              for (c = 0; c < nchars; c += 1) {
                ch = line[c]
                // If character is not the first character
                if (c !== 0) {
                  // Rotate character onto arc
                  ctx.rotate(PI * params.letterSpacing)
                }
                ctx.save()
                ctx.translate(0, -params.radius)
                if (params.flipArcText) {
                  ctx.scale(-1, -1)
                }
                ctx.fillText(ch, 0, 0)
                // Prevent extra shadow created by stroke (but only when fill is present)
                if (params.fillStyle !== 'transparent') {
                  ctx.shadowColor = 'transparent'
                }
                if (params.strokeWidth !== 0) {
                  // Only stroke if the stroke is not 0
                  ctx.strokeText(ch, 0, 0)
                }
                ctx.restore()
              }
              params.radius -= fontSize
              params.letterSpacing += fontSize / (constantCloseness * 2 * PI)
              ctx.restore()
            }
          } else {
            // Draw each line of text separately
            for (l = 0; l < lines.length; l += 1) {
              line = lines[l]
              // Add line offset to center point, but subtract some to center everything
              y = params.y + (l * params.height / lines.length) - (((lines.length - 1) * params.height / lines.length) / 2)

              ctx.shadowColor = params.shadowColor

              // Fill & stroke text
              ctx.fillText(line, x, y)
              // Prevent extra shadow created by stroke (but only when fill is present)
              if (params.fillStyle !== 'transparent') {
                ctx.shadowColor = 'transparent'
              }
              if (params.strokeWidth !== 0) {
                // Only stroke if the stroke is not 0
                ctx.strokeText(line, x, y)
              }
            }
          }

          // Adjust bounding box according to text baseline
          y = 0
          if (params.baseline === 'top') {
            y += params.height / 2
          } else if (params.baseline === 'bottom') {
            y -= params.height / 2
          }

          // Detect jCanvas events
          if (params._event) {
            ctx.beginPath()
            ctx.rect(
              params.x - (params.width / 2),
              params.y - (params.height / 2) + y,
              params.width,
              params.height
            )
            _detectEvents($canvases[e], ctx, params)
            // Close path and configure masking
            ctx.closePath()
          }
          _restoreTransform(ctx, params)
        }
      }
    }
    // Cache jCanvas parameters object for efficiency
    caches.propCache = params
    return $canvases
  }

  // Measures text width/height using the given parameters
  $.fn.measureText = function measureText (args) {
    var $canvases = this; var ctx
    var params; var lines

    // Attempt to retrieve layer
    params = $canvases.getLayer(args)
    // If layer does not exist or if returned object is not a jCanvas layer
    if (!params || (params && !params._layer)) {
      params = new jCanvasObject(args)
    }

    ctx = _getContext($canvases[0])
    if (ctx) {
      // Set canvas font using given properties
      _setCanvasFont($canvases[0], ctx, params)
      // Calculate width and height of text
      if (params.maxWidth !== null) {
        lines = _wrapText(ctx, params)
      } else {
        lines = params.text.split('\n')
      }
      _measureText($canvases[0], ctx, params, lines)
    }

    return params
  }

  /* Image API */

  // Draws image on canvas
  $.fn.drawImage = function drawImage (args) {
    var $canvases = this; var canvas; var e; var ctx; var data
    var params; var layer
    var img; var imgCtx; var source
    var imageCache = caches.imageCache

    // Draw image function
    function draw (canvas, ctx, data, params, layer) {
      // If width and sWidth are not defined, use image width
      if (params.width === null && params.sWidth === null) {
        params.width = params.sWidth = img.width
      }
      // If width and sHeight are not defined, use image height
      if (params.height === null && params.sHeight === null) {
        params.height = params.sHeight = img.height
      }

      // Ensure image layer's width and height are accurate
      if (layer) {
        layer.width = params.width
        layer.height = params.height
      }

      // Only crop image if all cropping properties are given
      if (params.sWidth !== null && params.sHeight !== null && params.sx !== null && params.sy !== null) {
        // If width is not defined, use the given sWidth
        if (params.width === null) {
          params.width = params.sWidth
        }
        // If height is not defined, use the given sHeight
        if (params.height === null) {
          params.height = params.sHeight
        }

        // Optionally crop from top-left corner of region
        if (params.cropFromCenter) {
          params.sx += params.sWidth / 2
          params.sy += params.sHeight / 2
        }

        // Ensure cropped region does not escape image boundaries

        // Top
        if ((params.sy - (params.sHeight / 2)) < 0) {
          params.sy = (params.sHeight / 2)
        }
        // Bottom
        if ((params.sy + (params.sHeight / 2)) > img.height) {
          params.sy = img.height - (params.sHeight / 2)
        }
        // Left
        if ((params.sx - (params.sWidth / 2)) < 0) {
          params.sx = (params.sWidth / 2)
        }
        // Right
        if ((params.sx + (params.sWidth / 2)) > img.width) {
          params.sx = img.width - (params.sWidth / 2)
        }

        _transformShape(canvas, ctx, params, params.width, params.height)
        _setGlobalProps(canvas, ctx, params)

        // Draw image
        ctx.drawImage(
          img,
          params.sx - (params.sWidth / 2),
          params.sy - (params.sHeight / 2),
          params.sWidth,
          params.sHeight,
          params.x - (params.width / 2),
          params.y - (params.height / 2),
          params.width,
          params.height
        )
      } else {
        // Show entire image if no crop region is defined

        _transformShape(canvas, ctx, params, params.width, params.height)
        _setGlobalProps(canvas, ctx, params)

        // Draw image on canvas
        ctx.drawImage(
          img,
          params.x - (params.width / 2),
          params.y - (params.height / 2),
          params.width,
          params.height
        )
      }

      // Draw invisible rectangle to allow for events and masking
      ctx.beginPath()
      ctx.rect(
        params.x - (params.width / 2),
        params.y - (params.height / 2),
        params.width,
        params.height
      )
      // Check for jCanvas events
      _detectEvents(canvas, ctx, params)
      // Close path and configure masking
      ctx.closePath()
      _restoreTransform(ctx, params)
      _enableMasking(ctx, data, params)
    }
    // On load function
    function onload (canvas, ctx, data, params, layer) {
      return function () {
        var $canvas = $(canvas)
        draw(canvas, ctx, data, params, layer)
        if (params.layer) {
          // Trigger 'load' event for layers
          _triggerLayerEvent($canvas, data, layer, 'load')
        } else if (params.load) {
          // Run 'load' callback for non-layers
          params.load.call($canvas[0], layer)
        }
        // Continue drawing successive layers after this image layer has loaded
        if (params.layer) {
          // Store list of previous masks for each layer
          layer._masks = data.transforms.masks.slice(0)
          if (params._next) {
            // Draw successive layers
            var complete = data.drawLayersComplete
            delete data.drawLayersComplete
            $canvas.drawLayers({
              clear: false,
              resetFire: true,
              index: params._next,
              complete: complete
            })
          }
        }
      }
    }
    for (e = 0; e < $canvases.length; e += 1) {
      canvas = $canvases[e]
      ctx = _getContext($canvases[e])
      if (ctx) {
        data = _getCanvasData($canvases[e])
        params = new jCanvasObject(args)
        layer = _addLayer($canvases[e], params, args, drawImage)
        if (params.visible) {
          // Cache the given source
          source = params.source

          imgCtx = source.getContext
          if (source.src || imgCtx) {
            // Use image or canvas element if given
            img = source
          } else if (source) {
            if (imageCache[source] && imageCache[source].complete) {
              // Get the image element from the cache if possible
              img = imageCache[source]
            } else {
              // Otherwise, get the image from the given source URL
              img = new Image()
              // If source URL is not a data URL
              if (!source.match(/^data:/i)) {
                // Set crossOrigin for this image
                img.crossOrigin = params.crossOrigin
              }
              img.src = source
              // Save image in cache for improved performance
              imageCache[source] = img
            }
          }

          if (img) {
            if (img.complete || imgCtx) {
              // Draw image if already loaded
              onload(canvas, ctx, data, params, layer)()
            } else {
              // Otherwise, draw image when it loads
              img.onload = onload(canvas, ctx, data, params, layer)
              // Fix onload() bug in IE9
              img.src = img.src
            }
          }
        }
      }
    }
    return $canvases
  }

  // Creates a canvas pattern object
  $.fn.createPattern = function createPattern (args) {
    var $canvases = this; var ctx
    var params
    var img; var imgCtx
    var pattern; var source

    // Function to be called when pattern loads
    function onload () {
      // Create pattern
      pattern = ctx.createPattern(img, params.repeat)
      // Run callback function if defined
      if (params.load) {
        params.load.call($canvases[0], pattern)
      }
    }

    ctx = _getContext($canvases[0])
    if (ctx) {
      params = new jCanvasObject(args)

      // Cache the given source
      source = params.source

      // Draw when image is loaded (if load() callback function is defined)

      if (isFunction(source)) {
        // Draw pattern using function if given

        img = $('<canvas />')[0]
        img.width = params.width
        img.height = params.height
        imgCtx = _getContext(img)
        source.call(img, imgCtx)
        onload()
      } else {
        // Otherwise, draw pattern using source image

        imgCtx = source.getContext
        if (source.src || imgCtx) {
          // Use image element if given
          img = source
        } else {
          // Use URL if given to get the image
          img = new Image()
          // If source URL is not a data URL
          if (!source.match(/^data:/i)) {
            // Set crossOrigin for this image
            img.crossOrigin = params.crossOrigin
          }
          img.src = source
        }

        // Create pattern if already loaded
        if (img.complete || imgCtx) {
          onload()
        } else {
          img.onload = onload
          // Fix onload() bug in IE9
          img.src = img.src
        }
      }
    } else {
      pattern = null
    }
    return pattern
  }

  // Creates a canvas gradient object
  $.fn.createGradient = function createGradient (args) {
    var $canvases = this; var ctx
    var params
    var gradient
    var stops = []; var nstops
    var start; var end
    var i; var a; var n; var p

    params = new jCanvasObject(args)
    ctx = _getContext($canvases[0])
    if (ctx) {
      // Gradient coordinates must be defined
      params.x1 = params.x1 || 0
      params.y1 = params.y1 || 0
      params.x2 = params.x2 || 0
      params.y2 = params.y2 || 0

      if (params.r1 !== null && params.r2 !== null) {
        // Create radial gradient if chosen
        gradient = ctx.createRadialGradient(params.x1, params.y1, params.r1, params.x2, params.y2, params.r2)
      } else {
        // Otherwise, create a linear gradient by default
        gradient = ctx.createLinearGradient(params.x1, params.y1, params.x2, params.y2)
      }

      // Count number of color stops
      for (i = 1; params['c' + i] !== undefined; i += 1) {
        if (params['s' + i] !== undefined) {
          stops.push(params['s' + i])
        } else {
          stops.push(null)
        }
      }
      nstops = stops.length

      // Define start stop if not already defined
      if (stops[0] === null) {
        stops[0] = 0
      }
      // Define end stop if not already defined
      if (stops[nstops - 1] === null) {
        stops[nstops - 1] = 1
      }

      // Loop through color stops to fill in the blanks
      for (i = 0; i < nstops; i += 1) {
        // A progression, in this context, is defined as all of the color stops between and including two known color stops

        if (stops[i] !== null) {
          // Start a new progression if stop is a number

          // Number of stops in current progression
          n = 1
          // Current iteration in current progression
          p = 0
          start = stops[i]

          // Look ahead to find end stop
          for (a = (i + 1); a < nstops; a += 1) {
            if (stops[a] !== null) {
              // If this future stop is a number, make it the end stop for this progression
              end = stops[a]
              break
            } else {
              // Otherwise, keep looking ahead
              n += 1
            }
          }

          // Ensure start stop is not greater than end stop
          if (start > end) {
            stops[a] = stops[i]
          }
        } else if (stops[i] === null) {
          // Calculate stop if not initially given
          p += 1
          stops[i] = start + (p * ((end - start) / n))
        }
        // Add color stop to gradient object
        gradient.addColorStop(stops[i], params['c' + (i + 1)])
      }
    } else {
      gradient = null
    }
    return gradient
  }

  // Manipulates pixels on the canvas
  $.fn.setPixels = function setPixels (args) {
    var $canvases = this
    var canvas; var e; var ctx; var canvasData
    var params
    var px
    var imgData; var pixelData; var i; var len

    for (e = 0; e < $canvases.length; e += 1) {
      canvas = $canvases[e]
      ctx = _getContext(canvas)
      canvasData = _getCanvasData($canvases[e])
      if (ctx) {
        params = new jCanvasObject(args)
        _addLayer(canvas, params, args, setPixels)
        _transformShape($canvases[e], ctx, params, params.width, params.height)

        // Use entire canvas of x, y, width, or height is not defined
        if (params.width === null || params.height === null) {
          params.width = canvas.width
          params.height = canvas.height
          params.x = params.width / 2
          params.y = params.height / 2
        }

        if (params.width !== 0 && params.height !== 0) {
          // Only set pixels if width and height are not zero

          imgData = ctx.getImageData(
            (params.x - (params.width / 2)) * canvasData.pixelRatio,
            (params.y - (params.height / 2)) * canvasData.pixelRatio,
            params.width * canvasData.pixelRatio,
            params.height * canvasData.pixelRatio
          )
          pixelData = imgData.data
          len = pixelData.length

          // Loop through pixels with the "each" callback function
          if (params.each) {
            for (i = 0; i < len; i += 4) {
              px = {
                r: pixelData[i],
                g: pixelData[i + 1],
                b: pixelData[i + 2],
                a: pixelData[i + 3]
              }
              params.each.call(canvas, px, params)
              pixelData[i] = px.r
              pixelData[i + 1] = px.g
              pixelData[i + 2] = px.b
              pixelData[i + 3] = px.a
            }
          }
          // Put pixels on canvas
          ctx.putImageData(
            imgData,
            (params.x - (params.width / 2)) * canvasData.pixelRatio,
            (params.y - (params.height / 2)) * canvasData.pixelRatio
          )
          // Restore transformation
          ctx.restore()
        }
      }
    }
    return $canvases
  }

  // Retrieves canvas image as data URL
  $.fn.getCanvasImage = function getCanvasImage (type, quality) {
    var $canvases = this; var canvas
    var dataURL = null
    if ($canvases.length !== 0) {
      canvas = $canvases[0]
      if (canvas.toDataURL) {
        // JPEG quality defaults to 1
        if (quality === undefined) {
          quality = 1
        }
        dataURL = canvas.toDataURL('image/' + type, quality)
      }
    }
    return dataURL
  }

  // Scales canvas based on the device's pixel ratio
  $.fn.detectPixelRatio = function detectPixelRatio (callback) {
    var $canvases = this
    var canvas; var e; var ctx
    var devicePixelRatio; var backingStoreRatio; var ratio
    var oldWidth; var oldHeight
    var data

    for (e = 0; e < $canvases.length; e += 1) {
      // Get canvas and its associated data
      canvas = $canvases[e]
      ctx = _getContext(canvas)
      data = _getCanvasData($canvases[e])

      // If canvas has not already been scaled with this method
      if (!data.scaled) {
        // Determine device pixel ratios
        devicePixelRatio = window.devicePixelRatio || 1
        backingStoreRatio = ctx.webkitBackingStorePixelRatio ||
                    ctx.mozBackingStorePixelRatio ||
                    ctx.msBackingStorePixelRatio ||
                    ctx.oBackingStorePixelRatio ||
                    ctx.backingStorePixelRatio || 1

        // Calculate general ratio based on the two given ratios
        ratio = devicePixelRatio / backingStoreRatio

        if (ratio !== 1) {
          // Scale canvas relative to ratio

          // Get the current canvas dimensions for future use
          oldWidth = canvas.width
          oldHeight = canvas.height

          // Resize canvas relative to the determined ratio
          canvas.width = oldWidth * ratio
          canvas.height = oldHeight * ratio

          // Scale canvas back to original dimensions via CSS
          canvas.style.width = oldWidth + 'px'
          canvas.style.height = oldHeight + 'px'

          // Scale context to counter the manual scaling of canvas
          ctx.scale(ratio, ratio)
        }

        // Set pixel ratio on canvas data object
        data.pixelRatio = ratio
        // Ensure that this method can only be called once for any given canvas
        data.scaled = true

        // Call the given callback function with the ratio as its only argument
        if (callback) {
          callback.call(canvas, ratio)
        }
      }
    }
    return $canvases
  }

  // Clears the jCanvas cache
  jCanvas.clearCache = function clearCache () {
    var cacheName
    for (cacheName in caches) {
      if (Object.prototype.hasOwnProperty.call(caches, cacheName)) {
        caches[cacheName] = {}
      }
    }
  }

  // Enable canvas feature detection with $.support
  $.support.canvas = ($('<canvas />')[0].getContext !== undefined)

  // Export jCanvas functions
  extendObject(jCanvas, {
    defaults: defaults,
    setGlobalProps: _setGlobalProps,
    transformShape: _transformShape,
    detectEvents: _detectEvents,
    closePath: _closePath,
    setCanvasFont: _setCanvasFont,
    measureText: _measureText
  })
  $.jCanvas = jCanvas
  $.jCanvasObject = jCanvasObject
}))
